diff --git a/a_long_dianjing/celery.py b/a_long_dianjing/celery.py
index 7927a29..d98cbfb 100644
--- a/a_long_dianjing/celery.py
+++ b/a_long_dianjing/celery.py
@@ -1,12 +1,6 @@
"""
阿龙电竞 - Celery定时任务主配置
-生产环境就绪版本,支持精确延时任务和周期性任务
"""
-
-
-
-
-
import os
from celery import Celery
from celery.schedules import crontab
@@ -26,14 +20,9 @@ app.config_from_object('django.conf:settings', namespace='CELERY')
#app.autodiscover_tasks()
app.autodiscover_tasks(['yonghu.tasks', 'yonghu.ranking_tasks', 'dingdan.tasks', 'peizhi.tasks'])
-
-
# 配置周期性任务(Celery Beat Schedule)
app.conf.beat_schedule = {
-
-
-
# 月榜数据转移(每月最后一天23:50执行)
'yuebang_zhuanyi': {
'task': 'yonghu.ranking_tasks.zhuanyi_yuebang',
@@ -41,8 +30,6 @@ app.conf.beat_schedule = {
'options': {'queue': 'periodic_tasks', 'priority': 7},
},
-
-
# 日榜数据转移(每天23:55执行,在清零前)
'ribang_zhuanyi': {
'task': 'yonghu.ranking_tasks.zhuanyi_ribang',
@@ -51,8 +38,6 @@ app.conf.beat_schedule = {
'options': {'queue': 'periodic_tasks', 'priority': 7}, # 优先级高于清零任务
},
-
-
# 清理旧历史数据(每月1号凌晨1点执行)
'qingli_jiulishuju': {
'task': 'yonghu.ranking_tasks.qingli_jiulishuju',
@@ -60,21 +45,13 @@ app.conf.beat_schedule = {
'options': {'queue': 'periodic_tasks', 'priority': 4},
},
-
-
-
-
-
-
-
- # 🔥 补偿检查任务(每5分钟运行)
+ # 补偿检查任务(每5分钟运行)
'check_order_expire_task': {
'task': 'dingdan.tasks.check_order_expire_task',
'schedule': crontab(minute='*/5000'),
'options': {'queue': 'order_tasks', 'priority': 3},
},
-
# 1. 每日凌晨0点执行 - 清零任务
'daily_reset_task': {
'task': 'yonghu.tasks.daily_reset_task',
@@ -99,10 +76,6 @@ app.conf.beat_schedule = {
'kwargs': {}
},
-
-
-
-
# 4. 每天凌晨0点5分清理收支记录
'daily_sz_reset_task': {
'task': 'peizhi.tasks.daily_sz_reset_task',
@@ -197,4 +170,4 @@ app.conf.task_acks_late = True
app.conf.worker_disable_rate_limits = True
# 结果过期时间
-app.conf.result_expires = 3600 # 任务结果保留1小时
\ No newline at end of file
+app.conf.result_expires = 3600 # 任务结果保留1小时
diff --git a/a_long_dianjing/celery_app.py b/a_long_dianjing/celery_app.py
index 10629c2..140f6e0 100644
--- a/a_long_dianjing/celery_app.py
+++ b/a_long_dianjing/celery_app.py
@@ -1,10 +1,10 @@
-# celery_app.py
-import os
-from celery import Celery
-
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'a_long_dianjing.settings')
-
-app = Celery('a_long_dianjing')
-app.config_from_object('django.conf:settings', namespace='CELERY')
-# 手动导入任务模块,避免与已有 tasks.py 冲突
-app.autodiscover_tasks(['dingdan.tongzhi_tasks'])
\ No newline at end of file
+# celery_app.py
+import os
+from celery import Celery
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'a_long_dianjing.settings')
+
+app = Celery('a_long_dianjing')
+app.config_from_object('django.conf:settings', namespace='CELERY')
+# 手动导入任务模块,避免与已有 tasks.py 冲突
+app.autodiscover_tasks(['dingdan.notice_tasks'])
diff --git a/a_long_dianjing/settings.py b/a_long_dianjing/settings.py
index 60bb888..2e6ab89 100644
--- a/a_long_dianjing/settings.py
+++ b/a_long_dianjing/settings.py
@@ -1,329 +1,329 @@
-"""
-Django settings for a_long_dianjing project.
-"""
-from pathlib import Path
-from datetime import timedelta
-import os
-
-# 从密钥配置文件导入敏感信息
-try:
- import app_secrets
-except ImportError:
- raise ImportError("缺少 appapp_secrets.py 密钥配置文件,请参照项目说明创建")
-
-BASE_DIR = Path(__file__).resolve().parent.parent
-
-SECRET_KEY = app_secrets.SECRET_KEY
-DEBUG = False
-ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'www.abas.asia']
-
-# ==================== 应用配置 ====================
-INSTALLED_APPS = [
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'rest_framework',
- 'rest_framework_simplejwt',
- 'corsheaders',
- 'django_celery_results',
- 'celery',
- 'yonghu',
- 'dingdan',
- 'shangpin',
- 'peizhi',
- 'houtai',
- 'utils',
- 'shangdian',
- 'dengji',
-]
-
-MIDDLEWARE = [
- 'corsheaders.middleware.CorsMiddleware',
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
-]
-
-ROOT_URLCONF = 'a_long_dianjing.urls'
-
-TEMPLATES = [
- {
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [BASE_DIR / 'templates'],
- 'APP_DIRS': True,
- 'OPTIONS': {
- 'context_processors': [
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
- ],
- },
- },
-]
-
-WSGI_APPLICATION = 'a_long_dianjing.wsgi.application'
-
-# ==================== 数据库 ====================
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.mysql',
- 'NAME': app_secrets.DATABASE_NAME,
- 'USER': app_secrets.DATABASE_USER,
- 'HOST': app_secrets.DATABASE_HOST,
- 'PASSWORD': app_secrets.DATABASE_PASSWORD,
- 'PORT': app_secrets.DATABASE_PORT,
- 'OPTIONS': {
- 'charset': 'utf8mb4',
- 'init_command': "SET time_zone='+08:00'",
- }
- }
-}
-
-# ==================== 密码验证 ====================
-AUTH_PASSWORD_VALIDATORS = [
- {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
- {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
- {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
- {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
-]
-
-# ==================== 国际化 ====================
-LANGUAGE_CODE = 'zh-Hans'
-TIME_ZONE = 'Asia/Shanghai'
-USE_I18N = True
-USE_TZ = False
-
-STATIC_URL = 'static/'
-DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
-
-# ==================== JWT 配置 ====================
-SIMPLE_JWT = {
- 'ACCESS_TOKEN_LIFETIME': timedelta(days=36500),
- 'REFRESH_TOKEN_LIFETIME': timedelta(days=36500),
- 'ROTATE_REFRESH_TOKENS': False,
- 'BLACKLIST_AFTER_ROTATION': False,
- 'UPDATE_LAST_LOGIN': False,
- 'ALGORITHM': 'HS256',
- 'SIGNING_KEY': SECRET_KEY,
- 'VERIFYING_KEY': None,
- 'AUDIENCE': None,
- 'ISSUER': None,
- 'JWK_URL': None,
- 'LEEWAY': 0,
- 'AUTH_HEADER_TYPES': ('Bearer',),
- 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
- 'USER_ID_FIELD': 'yonghuid',
- 'USER_ID_CLAIM': 'yonghuid',
- 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
- 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
- 'TOKEN_TYPE_CLAIM': 'token_type',
- 'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',
- 'JTI_CLAIM': 'jti',
-}
-
-AUTHENTICATION_BACKENDS = [
- 'django.contrib.auth.backends.ModelBackend',
-]
-
-AUTH_USER_MODEL = 'yonghu.UserMain'
-
-# ==================== DRF 配置 ====================
-REST_FRAMEWORK = {
- 'DEFAULT_AUTHENTICATION_CLASSES': (
- 'rest_framework_simplejwt.authentication.JWTAuthentication',
- ),
- 'DEFAULT_PERMISSION_CLASSES': (
- 'rest_framework.permissions.IsAuthenticated',
- ),
- 'DEFAULT_THROTTLE_CLASSES': [
- 'rest_framework.throttling.AnonRateThrottle',
- 'rest_framework.throttling.UserRateThrottle',
- ],
- 'DEFAULT_THROTTLE_RATES': {
- 'anon': '100/minute',
- 'user': '1000/minute',
- },
- 'EXCEPTION_HANDLER': 'utils.exception_handler.custom_exception_handler',
- 'DEFAULT_RENDERER_CLASSES': [
- 'rest_framework.renderers.JSONRenderer',
- ],
-}
-
-# ==================== H5 域名 ====================
-H5_DOMAIN = 'https://www.abas.asia:666'
-
-# ==================== CORS 配置 ====================
-CORS_ALLOWED_ORIGINS = [
- "http://localhost:8080",
- "http://127.0.0.1:8080",
- "https://www.abas.asia",
- "https://www.abas.asia:666",
- "https://www.abas.asia:1234",
- "https://www.abas.asia:963",
- "http://localhost:5174",
- "http://localhost:5173",
- "http://localhost:3000",
- "http://localhost:3001",
-]
-CORS_ALLOW_CREDENTIALS = True
-CORS_ALLOW_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
-CORS_ALLOW_HEADERS = [
- "accept", "accept-encoding", "authorization", "content-type",
- "dnt", "origin", "user-agent", "x-csrftoken", "x-requested-with",
-]
-
-# ==================== 腾讯云 COS ====================
-COS_SECRET_ID = app_secrets.COS_SECRET_ID
-COS_SECRET_KEY = app_secrets.COS_SECRET_KEY
-COS_REGION = app_secrets.COS_REGION
-COS_BUCKET = app_secrets.COS_BUCKET
-COS_SCHEME = 'https'
-COS_DOMAIN = f'https://{COS_BUCKET}.cos.{COS_REGION}.myqcloud.com'
-COS_APPID = app_secrets.COS_APPID
-COS_STS_DURATION = 7200
-
-# ==================== 微信小程序 & 服务号 ====================
-WEIXIN_APPID = app_secrets.WEIXIN_APPID
-WEIXIN_SECRET = app_secrets.WEIXIN_SECRET
-WEIXIN_MCHID = app_secrets.WEIXIN_MCHID
-WEIXIN_SHANGHUMIYAO = app_secrets.WEIXIN_SHANGHUMIYAO
-WEIXIN_OFFICIAL_APPID = app_secrets.WEIXIN_OFFICIAL_APPID
-WEIXIN_OFFICIAL_SECRET = app_secrets.WEIXIN_OFFICIAL_SECRET
-WEIXIN_OFFICIAL_TOKEN = app_secrets.WEIXIN_OFFICIAL_TOKEN
-WEIXIN_OFFICIAL_ENCODING_AES_KEY = app_secrets.WEIXIN_OFFICIAL_ENCODING_AES_KEY
-WEIXIN_TEMPLATE_ID = app_secrets.WEIXIN_TEMPLATE_ID
-WEIXIN_TEMPLATE_MAX_PER_MINUTE = 950
-WEIXIN_TEMPLATE_BATCH_SIZE = 100
-WEIXIN_CERT_PATH = app_secrets.WEIXIN_CERT_PATH
-WEIXIN_KEY_PATH = app_secrets.WEIXIN_KEY_PATH
-
-# ==================== 微信支付回调地址 ====================
-WEIXIN_NOTIFY_URL = 'https://www.abas.asia/hqhd/dingdan/pay-notify/'
-WEIXIN_PAY_DESCRIPTION = "网络服务"
-YAJIN_PAY_DESCRIPTION = "打手押金充值"
-YAJIN_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/yajinhuitiao"
-JIFEN_PAY_DESCRIPTION = "打手积分补充"
-JIFEN_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/jifenhuitiao"
-HUIYUAN_PAY_DESCRIPTION = "打手会员充值"
-HUIYUAN_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/huiyuanhuitiao"
-KAOHE_PAY_DESCRIPTION = '考核金牌-缴纳考核费'
-KAOHE_PAY_NOTIFY_URL = 'https://www.abas.asia/hqhd/yonghu/kh_huitiao'
-KAOHE_PAY_DEFAULT_FEE = 5.00
-SHANGJIA_CZ_PREFIX = "CzSJ"
-SHANGJIA_CZ_PAY_DESCRIPTION = "阿龙电竞-商家余额充值"
-SHANGJIA_CZ_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/sjhuitiao"
-SHANGJIA_CZ_MIN_AMOUNT = 1
-SHANGJIA_CZ_MAX_AMOUNT = 10000
-FAKUAN_PAY_DESCRIPTION = '罚款缴纳'
-FAKUAN_PAY_NOTIFY_URL = 'https://www.abas.asia/hqhd/yonghu/fk_huitiao'
-
-# ==================== 订单ID前缀 ====================
-ORDER_PREFIX = {
- 'yajin': 'CzYJ',
- 'jifen': 'CzJF',
- 'huiyuan': 'CzHY',
-}
-
-# ==================== 腾讯云 IM ====================
-TENCENT_IM_CONFIG = {
- 'SDK_APP_ID': 0,
- 'SECRET_KEY': '',
-}
-
-# ==================== 阿里云短信 ====================
-ALIYUN_SMS_CONFIG = {
- 'ACCESS_KEY_ID': '',
- 'ACCESS_KEY_SECRET': '',
- 'SIGN_NAME': '',
- 'TEMPLATE_CODE': '',
- 'REGION_ID': 'cn-hangzhou',
-}
-
-# ==================== GoEasy IM ====================
-GOEASY_APPKEY = app_secrets.GOEASY_APPKEY
-GOEASY_SECRET = app_secrets.GOEASY_SECRET
-GOEASY_REST_URL = 'https://rest-hz.goeasy.io/v2/im/message'
-
-# ==================== Celery 配置 ====================
-REDIS_HOST = app_secrets.REDIS_HOST
-REDIS_PORT = app_secrets.REDIS_PORT
-REDIS_PASSWORD = app_secrets.REDIS_PASSWORD
-REDIS_DB_CELERY = app_secrets.REDIS_DB_CELERY
-REDIS_DB_RESULT = app_secrets.REDIS_DB_RESULT
-
-CACHES = {
- 'default': {
- 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
- 'LOCATION': f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/2',
- 'TIMEOUT': 300,
- 'OPTIONS': {}
- }
-}
-
-if REDIS_PASSWORD:
- CELERY_BROKER_URL = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_CELERY}'
- CELERY_RESULT_BACKEND = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_RESULT}'
-else:
- CELERY_BROKER_URL = f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_CELERY}'
- CELERY_RESULT_BACKEND = f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_RESULT}'
-
-CELERY_ACCEPT_CONTENT = ['json']
-CELERY_TASK_SERIALIZER = 'json'
-CELERY_RESULT_SERIALIZER = 'json'
-CELERY_TIMEZONE = 'Asia/Shanghai'
-CELERY_ENABLE_UTC = True
-CELERY_IMPORTS = ('dingdan.tongzhi_tasks',)
-CELERY_TASK_TIME_LIMIT = 300
-CELERY_TASK_SOFT_TIME_LIMIT = 240
-CELERY_WORKER_CONCURRENCY = 4
-CELERY_WORKER_PREFETCH_MULTIPLIER = 1
-CELERY_TASK_ACKS_LATE = True
-CELERY_WORKER_DISABLE_RATE_LIMITS = True
-CELERY_TASK_DEFAULT_QUEUE = 'default'
-CELERY_TASK_QUEUES = {
- 'default': {'exchange': 'default', 'routing_key': 'default'},
- 'order_tasks': {'exchange': 'order_tasks', 'routing_key': 'order_tasks'},
- 'periodic_tasks': {'exchange': 'periodic_tasks', 'routing_key': 'periodic_tasks'},
-}
-
-# ==================== 定时任务时间配置 ====================
-ORDER_EXPIRE_SECONDS = 48 * 60 * 60 # 48小时 = 172800秒
-DAILY_TASK_HOUR = 0
-DAILY_TASK_MINUTE = 30
-MONTHLY_TASK_DAY = 1
-MONTHLY_TASK_HOUR = 0
-MONTHLY_TASK_MINUTE = 0
-ORDER_CHECK_INTERVAL = 5
-
-# ==================== 跨平台订单互通 ====================
-CROSS_PLATFORM_TOKEN = app_secrets.CROSS_PLATFORM_TOKEN
-CROSS_PLATFORM_SYNC_TOKEN = app_secrets.CROSS_PLATFORM_SYNC_TOKEN
-PARTNER_BASE_URL = app_secrets.PARTNER_BASE_URL
-PARTNER_SYNC_ORDER_URL = '/dingdan/partner_sync_order'
-PARTNER_CHECK_CLAIM_URL = '/dingdan/partner_check_claim'
-PARTNER_NOTIFY_CLAIM_URL = '/dingdan/partner_notify_claim'
-PARTNER_REFUND_NOTIFY_URL = '/dingdan/partner_refund_notify'
-AGENT_SHANGJIA_ID = app_secrets.AGENT_SHANGJIA_ID
-CROSS_PLATFORM_MEMBER_ID = app_secrets.CROSS_PLATFORM_MEMBER_ID
-CROSS_PLATFORM_GAME_TYPE_ID = 1
-CROSS_PLATFORM_WHITELIST = app_secrets.CROSS_PLATFORM_WHITELIST
-SPECIAL_GUANSHI_IDS = app_secrets.SPECIAL_GUANSHI_IDS
-
-# ==================== 微信支付 V3 ====================
-WECHAT_PAY_V3_CONFIG = {
- 'MCHID': app_secrets.WECHAT_PAY_V3_MCHID,
- 'APPID': app_secrets.WECHAT_PAY_V3_APPID,
- 'API_V3_KEY': app_secrets.WECHAT_PAY_V3_API_V3_KEY,
- 'PRIVATE_KEY_PATH': app_secrets.WECHAT_PAY_V3_PRIVATE_KEY_PATH,
- 'CERT_SERIAL_NO': app_secrets.WECHAT_PAY_V3_CERT_SERIAL_NO,
- 'PLATFORM_CERT_DIR': app_secrets.WECHAT_PAY_V3_PLATFORM_CERT_DIR,
- 'NOTIFY_URL': 'https://www.abas.asia/hqhd/yonghu/tixiancallback',
- 'TRANSFER_SCENE_ID': '1005',
-}
+"""
+Django settings for a_long_dianjing project.
+"""
+from pathlib import Path
+from datetime import timedelta
+import os
+
+# 从密钥配置文件导入敏感信息
+try:
+ import app_secrets
+except ImportError:
+ raise ImportError("缺少 appapp_secrets.py 密钥配置文件,请参照项目说明创建")
+
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+SECRET_KEY = app_secrets.SECRET_KEY
+DEBUG = False
+ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'www.abas.asia']
+
+# ==================== 应用配置 ====================
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'rest_framework',
+ 'rest_framework_simplejwt',
+ 'corsheaders',
+ 'django_celery_results',
+ 'celery',
+ 'yonghu',
+ 'dingdan',
+ 'shangpin',
+ 'peizhi',
+ 'houtai',
+ 'utils',
+ 'shangdian',
+ 'dengji',
+]
+
+MIDDLEWARE = [
+ 'corsheaders.middleware.CorsMiddleware',
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'a_long_dianjing.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [BASE_DIR / 'templates'],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'a_long_dianjing.wsgi.application'
+
+# ==================== 数据库 ====================
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': app_secrets.DATABASE_NAME,
+ 'USER': app_secrets.DATABASE_USER,
+ 'HOST': app_secrets.DATABASE_HOST,
+ 'PASSWORD': app_secrets.DATABASE_PASSWORD,
+ 'PORT': app_secrets.DATABASE_PORT,
+ 'OPTIONS': {
+ 'charset': 'utf8mb4',
+ 'init_command': "SET time_zone='+08:00'",
+ }
+ }
+}
+
+# ==================== 密码验证 ====================
+AUTH_PASSWORD_VALIDATORS = [
+ {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
+ {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
+ {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
+ {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
+]
+
+# ==================== 国际化 ====================
+LANGUAGE_CODE = 'zh-Hans'
+TIME_ZONE = 'Asia/Shanghai'
+USE_I18N = True
+USE_TZ = False
+
+STATIC_URL = 'static/'
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+# ==================== JWT 配置 ====================
+SIMPLE_JWT = {
+ 'ACCESS_TOKEN_LIFETIME': timedelta(days=36500),
+ 'REFRESH_TOKEN_LIFETIME': timedelta(days=36500),
+ 'ROTATE_REFRESH_TOKENS': False,
+ 'BLACKLIST_AFTER_ROTATION': False,
+ 'UPDATE_LAST_LOGIN': False,
+ 'ALGORITHM': 'HS256',
+ 'SIGNING_KEY': SECRET_KEY,
+ 'VERIFYING_KEY': None,
+ 'AUDIENCE': None,
+ 'ISSUER': None,
+ 'JWK_URL': None,
+ 'LEEWAY': 0,
+ 'AUTH_HEADER_TYPES': ('Bearer',),
+ 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
+ 'USER_ID_FIELD': 'yonghuid',
+ 'USER_ID_CLAIM': 'yonghuid',
+ 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
+ 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
+ 'TOKEN_TYPE_CLAIM': 'token_type',
+ 'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',
+ 'JTI_CLAIM': 'jti',
+}
+
+AUTHENTICATION_BACKENDS = [
+ 'django.contrib.auth.backends.ModelBackend',
+]
+
+AUTH_USER_MODEL = 'yonghu.UserMain'
+
+# ==================== DRF 配置 ====================
+REST_FRAMEWORK = {
+ 'DEFAULT_AUTHENTICATION_CLASSES': (
+ 'rest_framework_simplejwt.authentication.JWTAuthentication',
+ ),
+ 'DEFAULT_PERMISSION_CLASSES': (
+ 'rest_framework.permissions.IsAuthenticated',
+ ),
+ 'DEFAULT_THROTTLE_CLASSES': [
+ 'rest_framework.throttling.AnonRateThrottle',
+ 'rest_framework.throttling.UserRateThrottle',
+ ],
+ 'DEFAULT_THROTTLE_RATES': {
+ 'anon': '100/minute',
+ 'user': '1000/minute',
+ },
+ 'EXCEPTION_HANDLER': 'utils.exception_handler.custom_exception_handler',
+ 'DEFAULT_RENDERER_CLASSES': [
+ 'rest_framework.renderers.JSONRenderer',
+ ],
+}
+
+# ==================== H5 域名 ====================
+H5_DOMAIN = 'https://www.abas.asia:666'
+
+# ==================== CORS 配置 ====================
+CORS_ALLOWED_ORIGINS = [
+ "http://localhost:8080",
+ "http://127.0.0.1:8080",
+ "https://www.abas.asia",
+ "https://www.abas.asia:666",
+ "https://www.abas.asia:1234",
+ "https://www.abas.asia:963",
+ "http://localhost:5174",
+ "http://localhost:5173",
+ "http://localhost:3000",
+ "http://localhost:3001",
+]
+CORS_ALLOW_CREDENTIALS = True
+CORS_ALLOW_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
+CORS_ALLOW_HEADERS = [
+ "accept", "accept-encoding", "authorization", "content-type",
+ "dnt", "origin", "user-agent", "x-csrftoken", "x-requested-with",
+]
+
+# ==================== 腾讯云 COS ====================
+COS_SECRET_ID = app_secrets.COS_SECRET_ID
+COS_SECRET_KEY = app_secrets.COS_SECRET_KEY
+COS_REGION = app_secrets.COS_REGION
+COS_BUCKET = app_secrets.COS_BUCKET
+COS_SCHEME = 'https'
+COS_DOMAIN = f'https://{COS_BUCKET}.cos.{COS_REGION}.myqcloud.com'
+COS_APPID = app_secrets.COS_APPID
+COS_STS_DURATION = 7200
+
+# ==================== 微信小程序 & 服务号 ====================
+WEIXIN_APPID = app_secrets.WEIXIN_APPID
+WEIXIN_SECRET = app_secrets.WEIXIN_SECRET
+WEIXIN_MCHID = app_secrets.WEIXIN_MCHID
+WEIXIN_SHANGHUMIYAO = app_secrets.WEIXIN_SHANGHUMIYAO
+WEIXIN_OFFICIAL_APPID = app_secrets.WEIXIN_OFFICIAL_APPID
+WEIXIN_OFFICIAL_SECRET = app_secrets.WEIXIN_OFFICIAL_SECRET
+WEIXIN_OFFICIAL_TOKEN = app_secrets.WEIXIN_OFFICIAL_TOKEN
+WEIXIN_OFFICIAL_ENCODING_AES_KEY = app_secrets.WEIXIN_OFFICIAL_ENCODING_AES_KEY
+WEIXIN_TEMPLATE_ID = app_secrets.WEIXIN_TEMPLATE_ID
+WEIXIN_TEMPLATE_MAX_PER_MINUTE = 950
+WEIXIN_TEMPLATE_BATCH_SIZE = 100
+WEIXIN_CERT_PATH = app_secrets.WEIXIN_CERT_PATH
+WEIXIN_KEY_PATH = app_secrets.WEIXIN_KEY_PATH
+
+# ==================== 微信支付回调地址 ====================
+WEIXIN_NOTIFY_URL = 'https://www.abas.asia/hqhd/dingdan/pay-notify/'
+WEIXIN_PAY_DESCRIPTION = "网络服务"
+YAJIN_PAY_DESCRIPTION = "打手押金充值"
+YAJIN_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/yajinhuitiao"
+JIFEN_PAY_DESCRIPTION = "打手积分补充"
+JIFEN_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/jifenhuitiao"
+HUIYUAN_PAY_DESCRIPTION = "打手会员充值"
+HUIYUAN_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/huiyuanhuitiao"
+KAOHE_PAY_DESCRIPTION = '考核金牌-缴纳考核费'
+KAOHE_PAY_NOTIFY_URL = 'https://www.abas.asia/hqhd/yonghu/kh_huitiao'
+KAOHE_PAY_DEFAULT_FEE = 5.00
+SHANGJIA_CZ_PREFIX = "CzSJ"
+SHANGJIA_CZ_PAY_DESCRIPTION = "阿龙电竞-商家余额充值"
+SHANGJIA_CZ_PAY_NOTIFY_URL = "https://www.abas.asia/hqhd/shangpin/sjhuitiao"
+SHANGJIA_CZ_MIN_AMOUNT = 1
+SHANGJIA_CZ_MAX_AMOUNT = 10000
+FAKUAN_PAY_DESCRIPTION = '罚款缴纳'
+FAKUAN_PAY_NOTIFY_URL = 'https://www.abas.asia/hqhd/yonghu/fk_huitiao'
+
+# ==================== 订单ID前缀 ====================
+ORDER_PREFIX = {
+ 'yajin': 'CzYJ',
+ 'jifen': 'CzJF',
+ 'huiyuan': 'CzHY',
+}
+
+# ==================== 腾讯云 IM ====================
+TENCENT_IM_CONFIG = {
+ 'SDK_APP_ID': 0,
+ 'SECRET_KEY': '',
+}
+
+# ==================== 阿里云短信 ====================
+ALIYUN_SMS_CONFIG = {
+ 'ACCESS_KEY_ID': '',
+ 'ACCESS_KEY_SECRET': '',
+ 'SIGN_NAME': '',
+ 'TEMPLATE_CODE': '',
+ 'REGION_ID': 'cn-hangzhou',
+}
+
+# ==================== GoEasy IM ====================
+GOEASY_APPKEY = app_secrets.GOEASY_APPKEY
+GOEASY_SECRET = app_secrets.GOEASY_SECRET
+GOEASY_REST_URL = 'https://rest-hz.goeasy.io/v2/im/message'
+
+# ==================== Celery 配置 ====================
+REDIS_HOST = app_secrets.REDIS_HOST
+REDIS_PORT = app_secrets.REDIS_PORT
+REDIS_PASSWORD = app_secrets.REDIS_PASSWORD
+REDIS_DB_CELERY = app_secrets.REDIS_DB_CELERY
+REDIS_DB_RESULT = app_secrets.REDIS_DB_RESULT
+
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
+ 'LOCATION': f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/2',
+ 'TIMEOUT': 300,
+ 'OPTIONS': {}
+ }
+}
+
+if REDIS_PASSWORD:
+ CELERY_BROKER_URL = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_CELERY}'
+ CELERY_RESULT_BACKEND = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_RESULT}'
+else:
+ CELERY_BROKER_URL = f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_CELERY}'
+ CELERY_RESULT_BACKEND = f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB_RESULT}'
+
+CELERY_ACCEPT_CONTENT = ['json']
+CELERY_TASK_SERIALIZER = 'json'
+CELERY_RESULT_SERIALIZER = 'json'
+CELERY_TIMEZONE = 'Asia/Shanghai'
+CELERY_ENABLE_UTC = True
+CELERY_IMPORTS = ('dingdan.notice_tasks',)
+CELERY_TASK_TIME_LIMIT = 300
+CELERY_TASK_SOFT_TIME_LIMIT = 240
+CELERY_WORKER_CONCURRENCY = 4
+CELERY_WORKER_PREFETCH_MULTIPLIER = 1
+CELERY_TASK_ACKS_LATE = True
+CELERY_WORKER_DISABLE_RATE_LIMITS = True
+CELERY_TASK_DEFAULT_QUEUE = 'default'
+CELERY_TASK_QUEUES = {
+ 'default': {'exchange': 'default', 'routing_key': 'default'},
+ 'order_tasks': {'exchange': 'order_tasks', 'routing_key': 'order_tasks'},
+ 'periodic_tasks': {'exchange': 'periodic_tasks', 'routing_key': 'periodic_tasks'},
+}
+
+# ==================== 定时任务时间配置 ====================
+ORDER_EXPIRE_SECONDS = 48 * 60 * 60 # 48小时 = 172800秒
+DAILY_TASK_HOUR = 0
+DAILY_TASK_MINUTE = 30
+MONTHLY_TASK_DAY = 1
+MONTHLY_TASK_HOUR = 0
+MONTHLY_TASK_MINUTE = 0
+ORDER_CHECK_INTERVAL = 5
+
+# ==================== 跨平台订单互通 ====================
+CROSS_PLATFORM_TOKEN = app_secrets.CROSS_PLATFORM_TOKEN
+CROSS_PLATFORM_SYNC_TOKEN = app_secrets.CROSS_PLATFORM_SYNC_TOKEN
+PARTNER_BASE_URL = app_secrets.PARTNER_BASE_URL
+PARTNER_SYNC_ORDER_URL = '/dingdan/partner_sync_order'
+PARTNER_CHECK_CLAIM_URL = '/dingdan/partner_check_claim'
+PARTNER_NOTIFY_CLAIM_URL = '/dingdan/partner_notify_claim'
+PARTNER_REFUND_NOTIFY_URL = '/dingdan/partner_refund_notify'
+AGENT_SHANGJIA_ID = app_secrets.AGENT_SHANGJIA_ID
+CROSS_PLATFORM_MEMBER_ID = app_secrets.CROSS_PLATFORM_MEMBER_ID
+CROSS_PLATFORM_GAME_TYPE_ID = 1
+CROSS_PLATFORM_WHITELIST = app_secrets.CROSS_PLATFORM_WHITELIST
+SPECIAL_GUANSHI_IDS = app_secrets.SPECIAL_GUANSHI_IDS
+
+# ==================== 微信支付 V3 ====================
+WECHAT_PAY_V3_CONFIG = {
+ 'MCHID': app_secrets.WECHAT_PAY_V3_MCHID,
+ 'APPID': app_secrets.WECHAT_PAY_V3_APPID,
+ 'API_V3_KEY': app_secrets.WECHAT_PAY_V3_API_V3_KEY,
+ 'PRIVATE_KEY_PATH': app_secrets.WECHAT_PAY_V3_PRIVATE_KEY_PATH,
+ 'CERT_SERIAL_NO': app_secrets.WECHAT_PAY_V3_CERT_SERIAL_NO,
+ 'PLATFORM_CERT_DIR': app_secrets.WECHAT_PAY_V3_PLATFORM_CERT_DIR,
+ 'NOTIFY_URL': 'https://www.abas.asia/hqhd/yonghu/tixiancallback',
+ 'TRANSFER_SCENE_ID': '1005',
+}
diff --git a/houtai/_init_.py b/backend/_init_.py
similarity index 100%
rename from houtai/_init_.py
rename to backend/_init_.py
diff --git a/houtai/apps.py b/backend/apps.py
similarity index 86%
rename from houtai/apps.py
rename to backend/apps.py
index bc21450..f64e2fd 100644
--- a/houtai/apps.py
+++ b/backend/apps.py
@@ -2,4 +2,4 @@ from django.apps import AppConfig
class HoutaiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
- name = 'houtai'
\ No newline at end of file
+ name = 'houtai'
diff --git a/houtai/migrations/0001_initial.py b/backend/migrations/0001_initial.py
similarity index 100%
rename from houtai/migrations/0001_initial.py
rename to backend/migrations/0001_initial.py
diff --git a/houtai/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py b/backend/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py
similarity index 100%
rename from houtai/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py
rename to backend/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py
diff --git a/houtai/migrations/0003_tixianritongji.py b/backend/migrations/0003_tixianritongji.py
similarity index 100%
rename from houtai/migrations/0003_tixianritongji.py
rename to backend/migrations/0003_tixianritongji.py
diff --git a/dengji/__init__.py b/backend/migrations/__init__.py
similarity index 100%
rename from dengji/__init__.py
rename to backend/migrations/__init__.py
diff --git a/houtai/migrations/_init_.py b/backend/migrations/_init_.py
similarity index 100%
rename from houtai/migrations/_init_.py
rename to backend/migrations/_init_.py
diff --git a/houtai/models.py b/backend/models.py
similarity index 100%
rename from houtai/models.py
rename to backend/models.py
diff --git a/houtai/serializers.py b/backend/serializers.py
similarity index 95%
rename from houtai/serializers.py
rename to backend/serializers.py
index 68c7b37..8c1f8fd 100644
--- a/houtai/serializers.py
+++ b/backend/serializers.py
@@ -2,7 +2,7 @@
#小程序弹窗序列化器
from rest_framework import serializers
-from peizhi.models import PopupPage, PopupConfig, PopupImage
+from config.models import PopupPage, PopupConfig, PopupImage
class PopupImageSerializer(serializers.ModelSerializer):
"""图片序列化器(输出用)"""
diff --git a/houtai/urls.py b/backend/urls.py
similarity index 100%
rename from houtai/urls.py
rename to backend/urls.py
diff --git a/houtai/utils.py b/backend/utils.py
similarity index 99%
rename from houtai/utils.py
rename to backend/utils.py
index 95fc5bb..3299e0b 100644
--- a/houtai/utils.py
+++ b/backend/utils.py
@@ -11,6 +11,8 @@ from django.db.models import F
from datetime import date
from decimal import Decimal
+
+
logger = logging.getLogger(__name__)
@@ -217,10 +219,6 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
根据操作行为更新管事每日统计(邀请打手、充值会员、订单/押金分红)
action: 1 = 邀请打手, 2 = 充值会员, 3 = 商家订单分红, 4 = 押金分红
"""
- from django.db import transaction
- from django.db.models import F
- from datetime import date
-
today = date.today()
with transaction.atomic():
diff --git a/houtai/view.py b/backend/view.py
similarity index 96%
rename from houtai/view.py
rename to backend/view.py
index f902651..49af8c9 100644
--- a/houtai/view.py
+++ b/backend/view.py
@@ -1,12509 +1,12064 @@
-import hmac
-import threading
-import traceback
-from calendar import monthrange
-
-from django.contrib.auth.hashers import make_password
-from django.db.models import Q, Count
-from django.core.paginator import Paginator
-
-
-from houtai.utils import update_dashou_daily_by_action
-from dingdan.models import Lilubiao
-
-import uuid
-
-from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
-
-from utils.oss_utils import upload_to_oss, validate_image, delete_from_oss
-from shangpin.models import Shangpin, ShangpinLeixing, ShangpinZhuanqu,Huiyuan
-
-
-import hashlib
-from django.db.models import Q, Sum, F
-from rest_framework import status
-
-from houtai.models import Role, Permission, RolePermission, UserRole, TixianRiTongji
-from houtai.utils import verify_kefu_permission
-import xmltodict
-
-from django.conf import settings
-
-from dingdan.models import Dashoutupian, Chufajilu, PreSettlement, OrderDashouHistory
-# apps/houtai/views.py 继续添加
-
-from django.views.decorators.csrf import csrf_exempt
-from django.utils.decorators import method_decorator
-
-from houtai.models import Role
-
-from yonghu.models import UserMain, UserGuanshi, UserBoss, UserZuzhang
-
-from shangpin.models import DuociFenhong
-
-import time
-import logging
-import requests
-from datetime import date, datetime, timedelta
-from django.utils import timezone
-
-from decimal import Decimal
-
-from django.core.exceptions import ObjectDoesNotExist
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated, AllowAny
-
-from rest_framework.response import Response
-from rest_framework.parsers import JSONParser
-
-from dingdan.utils import update_daily_dispatch_stat
-from shangpin.models import ShangpinLeixing, Huiyuangoumai, Huiyuan
-from yonghu.models import UserMain, UserKefu, UserDashou, Xiugaijilu, UserShangjia
-from peizhi.models import AccountPermission, ClubConfig, Club, DailyDispatchStat, OrderTypeMapping, TixianQuotaDefault
-from dingdan.models import Dingdan, Tuikuanjilu, CrossPlatformOrderData
-
-# views.py
-import os
-from peizhi.models import PopupPage, PopupConfig, PopupImage
-from .serializers import PopupPageSerializer, PopupConfigSerializer, PopupImageSerializer
-
-
-logger = logging.getLogger(__name__)
-
-
-class GetClubConfigView(APIView):
- """
- 获取对方俱乐部配置列表(用于跨平台选择)
- 权限要求:客服身份 + 权限代码1且开启
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- """
- 处理 POST 请求,返回对方俱乐部配置数据(is_self=0)
- """
- try:
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '003aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
-
- # 5. 查询所有对方俱乐部(is_self=0),只取必要字段
- club_configs = ClubConfig.objects.filter(
- is_self=0 # 只返回对方俱乐部
- ).only(
- 'club_id',
- 'club_nickname',
- 'club_avatar',
- 'storage_bucket_domain' # 如果前端需要存储桶域名,一并返回
- )
-
- # 构造返回数据列表
- data_list = []
- for club in club_configs:
- data_list.append({
- 'club_id': club.club_id,
- 'club_nickname': club.club_nickname or club.club_id, # 若昵称为空,用ID代替
- 'club_avatar': club.club_avatar or '', # 可能为空
- 'storage_bucket_domain': club.storage_bucket_domain or '' # 可选
- })
-
- return Response({
- 'code': 0,
- 'msg': 'success',
- 'data': data_list
- })
-
- except Exception as e:
- logger.error(f"获取俱乐部配置失败: {str(e)}")
- return Response({
- 'code': 500,
- 'msg': '服务器内部错误'
- })
-
-
-
-
-
-class GetCrossOrderListView(APIView):
- """
- 获取跨平台订单列表(客服后台)
- 权限要求:客服身份 + 权限代码1且开启
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- """
- 处理 POST 请求,返回分页订单数据及统计信息
- """
- try:
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '003aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看俱乐部订单'})
-
- # 5. 获取请求参数
- data = request.data
-
-
-
-
- # 分页参数
- page = int(data.get('page', 1))
- page_size = int(data.get('pageSize', 20))
-
- # 查询参数
- leixing = data.get('leixing') # 订单类型ID
- zhuangtai = data.get('zhuangtai') # 状态字符串,如 "1,7"
- club_id = data.get('club_id') # 对方俱乐部ID(精确)
- direction = data.get('direction') # send/receive/all
- dingdan_id = data.get('dingdan_id') # 订单ID模糊搜索
- club_id_search = data.get('club_id_search') # 对方俱乐部ID模糊搜索
- date = data.get('date') # 日期 YYYY-MM-DD
- dashou_id = data.get('dashou_id') # 打手ID(接单打手ID)
- clkf = data.get('clkf') # 处理客服
-
- # 6. 构建基础查询(跨平台订单)
- base_q = Q(is_cross=1)
-
- # 添加筛选条件(非空且非全部)
- if leixing and leixing != '':
- base_q &= Q(leixing_id=leixing)
-
- if zhuangtai and zhuangtai != 'all':
- # 解析逗号分隔的状态码
- status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip().isdigit()]
- if status_list:
- base_q &= Q(zhuangtai__in=status_list)
-
- if club_id and club_id != '':
- base_q &= Q(partner_club_id=club_id)
-
- # 方向映射
- if direction == 'send':
- base_q &= Q(dispatch_type=1)
- elif direction == 'receive':
- base_q &= Q(dispatch_type=2)
- # direction == 'all' 时不加方向条件
-
- if dingdan_id:
- base_q &= Q(dingdan_id__icontains=dingdan_id)
-
- if club_id_search:
- base_q &= Q(partner_club_id__icontains=club_id_search)
-
- if date:
- # 按日期精确过滤(假设 create_time 是带时区的 DateTimeField)
- base_q &= Q(create_time__date=date)
-
- if dashou_id:
- base_q &= Q(jiedan_dashou_id=dashou_id)
-
- if clkf:
- base_q &= Q(clkf__icontains=clkf)
-
- # 7. 统计各状态数量(基于相同过滤条件)
- # 获取所有符合条件的订单的状态分组统计
- status_counts = Dingdan.objects.filter(base_q).values('zhuangtai').annotate(count=Count('id')).order_by()
-
- # 转换为字典 {状态码: 数量}
- status_map = {item['zhuangtai']: item['count'] for item in status_counts}
-
- # 定义状态列表(与前端一致)
- status_list_map = {
- 'all': 0, # 稍后填充总数量
- '1,7': 0,
- '2': 0,
- '8': 0,
- '3': 0,
- '4': 0,
- '5': 0,
- '6': 0,
- }
- # 填充总数
- status_list_map['all'] = sum(status_map.values())
- # 填充单个状态
- for code, cnt in status_map.items():
- if code == 1 or code == 7:
- status_list_map['1,7'] += cnt
- elif code in status_list_map:
- status_list_map[str(code)] = cnt
- # 其他状态暂不处理
-
- # 构建 stats 统计
- total_orders = status_list_map['all']
- completed_orders = status_map.get(3, 0)
- refund_orders = status_map.get(5, 0)
- pending_orders = status_map.get(8, 0) + status_map.get(4, 0)
-
- stats = {
- 'totalOrders': total_orders,
- 'completedOrders': completed_orders,
- 'refundOrders': refund_orders,
- 'pendingOrders': pending_orders,
- }
-
- # 8. 分页获取订单列表
- # 按创建时间倒序,保证分页稳定
- orders_qs = Dingdan.objects.filter(base_q).order_by('-create_time')
-
- # 使用 Paginator 分页
- paginator = Paginator(orders_qs, page_size)
- current_page = paginator.get_page(page)
- order_list = current_page.object_list
-
- # 只返回前端需要的字段,减少数据传输
- data_list = []
- for order in order_list:
- data_list.append({
- 'dingdan_id': order.dingdan_id,
- 'nicheng': order.nicheng or '',
- 'jieshao': order.jieshao or '',
- 'jiage': float(order.jine) if order.jine else 0,
- 'zhuangtai': order.zhuangtai,
- })
-
- # 9. 返回结果
- return Response({
- 'code': 0,
- 'msg': 'success',
- 'data': {
- 'list': data_list,
- 'total': total_orders,
- 'statusCounts': status_list_map,
- 'stats': stats,
- }
- })
-
- except Exception as e:
- logger.error(f"获取跨平台订单列表失败: {str(e)}")
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-
-
-
-class GetCrossOrderDetailView(APIView):
- """
- 客服获取跨平台订单详情接口
- 请求:POST /houtai/hqkptddxq
- 参数:{"username": "客服手机号", "dingdan_id": "订单ID"}
- 认证:JWT,权限码1
- 返回:code=0 + 订单详情数据(包含俱乐部信息、打手图片、处罚记录等)
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- start_time = time.time()
-
- # 1. 获取参数
- username = request.data.get('username', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- if not username or not dingdan_id:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- try:
- # 2. 身份验证
- current_user = request.user
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '003aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看俱乐部订单'})
-
- # 4. 查询订单
- dingdan_obj = Dingdan.objects.select_related(
- 'pingtai_kuozhan',
- 'shangjia_kuozhan'
- ).get(dingdan_id=dingdan_id)
-
- # 5. 构建基础响应数据
- response_data = {
- 'dingdan_id': dingdan_obj.dingdan_id or '',
- 'zhuangtai': dingdan_obj.zhuangtai or 0,
- 'fadanpingtai': dingdan_obj.fadan_pingtai or 0,
- 'dispatch_type': dingdan_obj.dispatch_type or 0, # 1=我方派单, 2=对方派单
- 'is_cross': dingdan_obj.is_cross or 0,
- 'partner_order_id': dingdan_obj.partner_order_id or '',
- 'partner_club_id': dingdan_obj.partner_club_id or '',
- 'jine': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00,
- 'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00,
- 'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '',
- 'dashou_liuyan': dingdan_obj.dashou_liuyan or '',
- 'zhiding_id': dingdan_obj.zhiding_id or '',
- 'shangpin_id': dingdan_obj.shangpin_id or '',
- 'leixing': dingdan_obj.leixing_id or 0,
- 'tupian_url': dingdan_obj.tupian or '',
- 'jieshao': dingdan_obj.jieshao or '',
- 'beizhu': dingdan_obj.beizhu or '',
- 'tkly': dingdan_obj.tkly or '',
- 'nicheng': dingdan_obj.nicheng or '',
- 'clkf': dingdan_obj.clkf or '',
- 'chuangjianshijian': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.create_time else '',
- 'genggaishijian': dingdan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.update_time else '',
- }
-
- # 6. 扩展表数据
- if dingdan_obj.fadan_pingtai == 1:
- if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan:
- response_data['pingtai_kuozhan'] = {
- 'laoban_id': dingdan_obj.pingtai_kuozhan.laoban_id or '',
- 'laoban_pingjia': dingdan_obj.pingtai_kuozhan.laoban_pingjia or '',
- }
- else:
- response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''}
- elif dingdan_obj.fadan_pingtai == 2:
- if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan:
- shangjia = dingdan_obj.shangjia_kuozhan
- response_data['shangjia_kuozhan'] = {
- 'shangjia_id': shangjia.shangjia_id or '',
- 'sjnicheng': shangjia.sjnicheng or '',
- 'shangjia_pingjia': shangjia.shangjia_pingjia or '',
- 'cfliyou': shangjia.cfliyou or '',
- 'sqzhuangtai': shangjia.sqzhuangtai if shangjia.sqzhuangtai is not None else 0,
- 'bhliyou': shangjia.bhliyou or '',
- }
- else:
- response_data['shangjia_kuozhan'] = {
- 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '',
- 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': ''
- }
-
- # ========== 核心逻辑:判断派单/接单关系 ==========
- dispatch_type = dingdan_obj.dispatch_type # 1=我方派单, 2=对方派单
- is_cross = dingdan_obj.is_cross
- partner_club_id = dingdan_obj.partner_club_id
- partner_order_id = dingdan_obj.partner_order_id
-
- # 获取我方俱乐部配置(用于本地存储桶域名)
- my_club_config = ClubConfig.objects.filter(is_self=1).first()
- my_storage_domain = my_club_config.storage_bucket_domain if my_club_config else ''
-
- # 初始化字段
- response_data['club_info'] = None # 对方俱乐部信息
- response_data['partner_images'] = [] # 打手提交图片(对方平台打手或我方打手)
- response_data['punishment_info'] = {} # 处罚信息
- response_data['is_punished'] = False # 是否已处罚(用于前端禁用按钮)
- response_data['receive_side'] = '' # 接单方:'self' 我方接单,'partner' 对方接单
- response_data['dispatch_side'] = '' # 派单方:'self' 我方派单,'partner' 对方派单
-
- # 设置派单方
- if dispatch_type == 1:
- response_data['dispatch_side'] = 'self'
- elif dispatch_type == 2:
- response_data['dispatch_side'] = 'partner'
- else:
- # 兼容旧数据:根据 partner_club_id 或 partner_order_id 推断
- if partner_club_id or partner_order_id:
- response_data['dispatch_side'] = 'partner'
- else:
- response_data['dispatch_side'] = 'self'
-
- # 情况1:对方派单(对方平台发单,我方打手接单)
- if response_data['dispatch_side'] == 'partner':
- # 接单方为我方
- response_data['receive_side'] = 'self'
-
- # 获取对方俱乐部信息(用于展示)
- if partner_club_id:
- partner_config = ClubConfig.objects.filter(club_id=partner_club_id, is_self=0).first()
- if partner_config:
- response_data['club_info'] = {
- 'club_id': partner_config.club_id,
- 'club_nickname': partner_config.club_nickname or partner_config.club_id,
- 'club_avatar': partner_config.club_avatar or '',
- }
- else:
- response_data['club_info'] = {'club_id': partner_club_id, 'club_nickname': '', 'club_avatar': ''}
- else:
- response_data['club_info'] = None
-
- # 查询我方打手提交的图片(本地)
- dashou_images = Dashoutupian.objects.filter(
- dingdan_id=dingdan_id,
- dashou=dingdan_obj.jiedan_dashou_id
- ).values_list('tupian', flat=True)
- images = []
- for rel_path in dashou_images:
- if rel_path:
- if rel_path.startswith('http'):
- images.append(rel_path)
- else:
- images.append(my_storage_domain + rel_path)
- response_data['partner_images'] = images
-
- # 查询我方打手处罚记录
- punish_records = Chufajilu.objects.filter(
- dingdan_id=dingdan_id,
- dashouid=dingdan_obj.jiedan_dashou_id
- ).order_by('-create_time')
- if punish_records.exists():
- latest = punish_records.first()
- response_data['punishment_info'] = {
- 'cfliyou': latest.cfliyou or '',
- 'sqzhuangtai': latest.sqzhuangtai or 0,
- 'bhliyou': latest.bhliyou or '',
- 'ssliyou': latest.ssliyou or '',
- 'jifen': latest.jifen or 0,
- }
- response_data['is_punished'] = True
- else:
- response_data['punishment_info'] = {}
- response_data['is_punished'] = False
-
- # 情况2:我方派单(我方平台发单)
- else:
- response_data['dispatch_side'] = 'self'
- # 根据是否有对方订单ID/俱乐部ID判断接单方
- if partner_club_id or partner_order_id:
- # 有对方信息,说明订单派给了对方平台,对方接单
- response_data['receive_side'] = 'partner'
-
- # 获取对方俱乐部信息
- if partner_club_id:
- partner_config = ClubConfig.objects.filter(club_id=partner_club_id, is_self=0).first()
- if partner_config:
- response_data['club_info'] = {
- 'club_id': partner_config.club_id,
- 'club_nickname': partner_config.club_nickname or partner_config.club_id,
- 'club_avatar': partner_config.club_avatar or '',
- }
- else:
- response_data['club_info'] = {'club_id': partner_club_id, 'club_nickname': '', 'club_avatar': ''}
-
- # 需要调用对方接口获取图片和处罚记录
- if partner_club_id and partner_order_id:
- # 获取对方服务器域名
- if my_club_config:
- self_club_id = my_club_config.club_id
- else:
- logger.error(f"无法获取本俱乐部ID,订单ID: {dingdan_id}")
- return Response({'code': 500, 'msg': '系统配置错误,请联系管理员'})
-
- try:
- club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
- partner_domain = club_rel.partner_domain
- except ObjectDoesNotExist:
- logger.error(f"未找到合作关系: {self_club_id} <-> {partner_club_id}")
- return Response({'code': 500, 'msg': '未找到对方平台配置'})
-
- # 调用对方接口,最多重试3次
- url = partner_domain.rstrip('/') + '/houtai/kpthqtp'
- payload = {
- 'order_id': partner_order_id,
- 'club_id': self_club_id,
- }
- timeout = 3
- retries = 3
- success = False
- for attempt in range(retries):
- try:
- resp = requests.post(url, json=payload, timeout=timeout)
- if resp.status_code == 200:
- resp_json = resp.json()
- if resp_json.get('code') == 0:
- data = resp_json.get('data', {})
- response_data['partner_images'] = data.get('images', [])
- response_data['punishment_info'] = data.get('punishment', {})
- response_data['is_punished'] = bool(response_data['punishment_info'].get('cfliyou'))
- success = True
- break
- else:
- logger.warning(f"对方接口返回错误 (尝试{attempt+1}): {resp_json.get('msg')}")
- else:
- logger.warning(f"对方接口HTTP错误 (尝试{attempt+1}): {resp.status_code}")
- except requests.exceptions.Timeout:
- logger.warning(f"请求对方接口超时 (尝试{attempt+1}): {url}")
- except Exception as e:
- logger.warning(f"请求对方接口异常 (尝试{attempt+1}): {str(e)}")
- if not success:
- # 三次失败,返回部分数据,但提示前端对方数据获取失败
- response_data['partner_images'] = []
- response_data['punishment_info'] = {}
- response_data['is_punished'] = False
- response_data['partner_data_error'] = '获取对方平台数据失败,请稍后重试'
- else:
- # 对方信息不完整,无法获取数据
- response_data['partner_images'] = []
- response_data['punishment_info'] = {}
- response_data['is_punished'] = False
- response_data['partner_data_error'] = '对方订单信息不完整'
- else:
- # 无对方信息,说明我方派单且我方接单
- response_data['receive_side'] = 'self'
- # 我方打手提交图片(本地)
- dashou_images = Dashoutupian.objects.filter(
- dingdan_id=dingdan_id,
- dashou=dingdan_obj.jiedan_dashou_id
- ).values_list('tupian', flat=True)
- images = []
- for rel_path in dashou_images:
- if rel_path:
- if rel_path.startswith('http'):
- images.append(rel_path)
- else:
- images.append(my_storage_domain + rel_path)
- response_data['partner_images'] = images
-
- # 处罚记录
- punish_records = Chufajilu.objects.filter(
- dingdan_id=dingdan_id,
- dashouid=dingdan_obj.jiedan_dashou_id
- ).order_by('-create_time')
- if punish_records.exists():
- latest = punish_records.first()
- response_data['punishment_info'] = {
- 'cfliyou': latest.cfliyou or '',
- 'sqzhuangtai': latest.sqzhuangtai or 0,
- 'bhliyou': latest.bhliyou or '',
- 'ssliyou': latest.ssliyou or '',
- 'jifen': latest.jifen or 0,
- }
- response_data['is_punished'] = True
- else:
- response_data['punishment_info'] = {}
- response_data['is_punished'] = False
-
- # 如果是我方派单且我方接单,对方俱乐部信息为空
- response_data['club_info'] = None
-
- # 7. 查询退款理由
- try:
- tuikuan_record = Tuikuanjilu.objects.filter(
- dingdan_id=dingdan_id
- ).order_by('-create_time').first()
- response_data['tuikuan_liyou'] = tuikuan_record.liyou if tuikuan_record else ''
- except Exception as e:
- logger.warning(f"查询退款记录失败: {str(e)}")
- response_data['tuikuan_liyou'] = ''
-
- # ========== 新增:查询更换打手生成的新订单信息 ==========
- try:
- from peizhi.models import ShangjiaLianjie
- new_lianjie = ShangjiaLianjie.objects.filter(
- previous_dingdan_id=dingdan_id
- ).order_by('-create_time').first()
- if new_lianjie:
- response_data['new_dingdan_id'] = new_lianjie.dingdan_id or ''
- response_data['new_lianjie'] = new_lianjie.lianjie or ''
- else:
- response_data['new_dingdan_id'] = ''
- response_data['new_lianjie'] = ''
- except Exception as e:
- logger.warning(f"查询新订单链接失败: {str(e)}")
- response_data['new_dingdan_id'] = ''
- response_data['new_lianjie'] = ''
- # =====================================================
-
- elapsed = time.time() - start_time
- logger.info(f"获取跨平台订单详情接口耗时: {elapsed:.3f}s, 订单ID: {dingdan_id}")
- return Response({'code': 0, 'data': response_data})
-
- except Dingdan.DoesNotExist:
- logger.warning(f"订单不存在: {dingdan_id}")
- return Response({'code': 404, 'msg': '订单不存在'})
- except Exception as e:
- logger.error(f"获取跨平台订单详情接口异常: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-class CrossPlatformRefundView(APIView):
- """
- 跨平台订单退款接口(同时支持本地订单和跨平台订单)
- 请求:POST /houtai/kptk
- 参数:{
- "username": "客服账号ID",
- "dingdan_id": "订单ID",
- "tuikuan_liyou": "退款理由(可选)"
- }
- 认证:JWT
- 返回:code=0 成功,其他失败
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- username = request.data.get('username', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
-
- if not username or not dingdan_id:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 身份验证
- current_user = request.user
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '003aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权处理订单'})
-
- # 4. 查询订单(带扩展表)
- try:
- order = Dingdan.objects.select_related(
- 'pingtai_kuozhan',
- 'shangjia_kuozhan'
- ).get(dingdan_id=dingdan_id)
- except Dingdan.DoesNotExist:
- logger.warning(f"订单不存在: {dingdan_id}")
- return Response({'code': 404, 'msg': '订单不存在'})
-
- # 5. 订单状态校验(只有 1,7,2,8,4 可退款)
- allowed_statuses = [1, 7, 2, 8, 4]
- if order.zhuangtai not in allowed_statuses:
- logger.warning(f"订单状态不允许退款: {order.zhuangtai}")
- return Response({'code': 400, 'msg': '订单状态不允许退款'})
-
- # 6. 根据派单方向分流
- dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单
-
- # ---------- 情况一:对方派单(我方打手接对方的单)----------
- if dispatch_type == 2:
- # 对方派单,资金在对方平台,我方只处理打手状态和订单状态
- with transaction.atomic():
- # 更新订单状态为已退款
- order.zhuangtai = 5
- order.clkf = current_user.phone
- if tuikuan_liyou:
- order.tkly = tuikuan_liyou
- order.save()
-
- # 更新打手数据(接单打手)
- dashou_id = order.jiedan_dashou_id
- if dashou_id:
- try:
- dashou_user = UserMain.objects.get(yonghuid=dashou_id)
- dashou_profile = dashou_user.dashou_profile
- dashou_profile.tuikuanliang += 1
- dashou_profile.zhuangtai = 1 # 设为空闲
- dashou_profile.save()
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"对方派单退款:打手 {dashou_id} 不存在,跳过更新")
-
- # 更新退款记录(记录操作)
- try:
- refund_record, created = Tuikuanjilu.objects.get_or_create(
- dingdan_id=dingdan_id,
- defaults={
- 'dashouid': dashou_id or '',
- 'qingqiuid': '',
- 'chuliid': current_user.phone,
- 'liyou': tuikuan_liyou or '客服退款(对方派单)',
- 'sqzhuangtai': 1,
- 'jine': order.jine or 0,
- 'dashou_fencheng': order.dashou_fencheng or 0,
- 'jieshao': order.jieshao or '',
- 'beizhu': '对方派单退款',
- 'nicheng': order.nicheng or '',
- }
- )
- if not created:
- refund_record.sqzhuangtai = 1
- refund_record.chuliid = current_user.phone
- if tuikuan_liyou:
- refund_record.liyou = tuikuan_liyou
- refund_record.save()
- except Exception as e:
- logger.error(f"更新退款记录失败: {str(e)}")
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.jiedan_dashou_id,
- amount=order.dashou_fencheng,
- action=3 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
-
- # ✅ 新方法(行为驱动,更安全)
- from houtai.utils import update_shangjia_daily
- shangjia_ext = order.shangjia_kuozhan
- shangjia_id = shangjia_ext.shangjia_id
- update_shangjia_daily(
- yonghuid=shangjia_id,
- amount=Decimal(str(order.jine)),
- action=3 # 3 = 退款
- )
-
- # 更新客服统计
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- logger.info(f"对方派单退款成功,订单 {dingdan_id}")
- return Response({'code': 0, 'msg': '退款成功'})
-
- # ---------- 情况二:我方派单(订单由我方发出)----------
- # 需要进一步判断是本地订单还是跨平台订单
- is_cross = order.is_cross
- partner_club_id = order.partner_club_id
- partner_order_id = order.partner_order_id
-
- # 子情况2.1:本地订单(非跨平台)
- if not is_cross or not partner_club_id:
- # 按原有逻辑处理:平台订单或商家订单
- if order.fadan_pingtai == 1:
- # 平台订单退款(调用微信支付)
- return self._refund_platform_order(order, current_user, tuikuan_liyou, kefu)
- elif order.fadan_pingtai == 2:
- # 商家订单退款(直接退余额)
- return self._refund_merchant_order(order, current_user, tuikuan_liyou, kefu)
- else:
- return Response({'code': 400, 'msg': '未知的发单平台类型'})
-
- # 子情况2.2:跨平台订单(我方派单给对方)
- else:
- # 必须先执行本地资金退款(因为钱在我方平台)
- # 根据发单平台类型执行退款
- local_refund_success = False
- try:
- with transaction.atomic():
- if order.fadan_pingtai == 1:
- # 平台订单:调用微信退款
- refund_result = self._call_wechat_refund(order)
- if refund_result['code'] != 0:
- raise Exception(refund_result['msg'])
- # 微信退款成功,更新订单状态
- order.zhuangtai = 5
- order.clkf = current_user.phone
- if tuikuan_liyou:
- order.tkly = tuikuan_liyou
- order.save()
- local_refund_success = True
- elif order.fadan_pingtai == 2:
- # 商家订单:退余额给商家
- self._refund_to_merchant(order, current_user.phone, tuikuan_liyou)
- order.zhuangtai = 5
- order.clkf = current_user.phone
- if tuikuan_liyou:
- order.tkly = tuikuan_liyou
- order.save()
- local_refund_success = True
- else:
- return Response({'code': 400, 'msg': '未知的发单平台类型'})
-
- # 更新打手数据(接单打手)
- dashou_id = order.jiedan_dashou_id
- if dashou_id:
- try:
- dashou_user = UserMain.objects.get(yonghuid=dashou_id)
- dashou_profile = dashou_user.dashou_profile
- dashou_profile.tuikuanliang += 1
- dashou_profile.zhuangtai = 1
- dashou_profile.save()
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"跨平台退款:打手 {dashou_id} 不存在,跳过更新")
-
- # 更新退款记录
- self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou)
-
- # 更新客服统计
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- except Exception as e:
- logger.error(f"跨平台订单本地退款失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': f'本地退款失败: {str(e)}'})
-
- # 本地退款成功后,通知对方平台
- if local_refund_success:
- # 查询对方平台域名
- try:
- # 获取我方俱乐部ID
- my_club_config = ClubConfig.objects.filter(is_self=1).first()
- if not my_club_config:
- logger.error("未配置我方俱乐部信息")
- # 本地已成功,仅记录错误,不影响返回
- else:
- self_club_id = my_club_config.club_id
- club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
- partner_domain = club_rel.partner_domain
-
- # 调用对方退款通知接口
- notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk'
- payload = {
- 'order_id': partner_order_id, # 对方订单ID
- 'club_id': self_club_id, # 我方俱乐部ID
- 'status': 5, # 通知对方我方已退款
- 'reason': tuikuan_liyou
- }
- # 设置超时3秒,不阻塞主流程
- try:
- resp = requests.post(notify_url, json=payload, timeout=3)
- if resp.status_code == 200:
- resp_json = resp.json()
- if resp_json.get('code') == 0:
- # 更新跨平台订单扩展表
- cross_data, _ = CrossPlatformOrderData.objects.get_or_create(
- dingdan_id=dingdan_id,
- defaults={
- 'partner_order_id': partner_order_id,
- 'partner_club_id': partner_club_id,
- 'partner_order_status': resp_json.get('data', {}).get('partner_status', 0),
- 'partner_dashou_id': resp_json.get('data', {}).get('partner_dashou_id', ''),
- 'partner_amount': order.jine or 0,
- }
- )
- if not _:
- cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0)
- cross_data.save()
- else:
- logger.warning(f"对方平台返回错误: {resp_json.get('msg')}")
- else:
- logger.warning(f"对方平台接口HTTP错误: {resp.status_code}")
- except requests.exceptions.Timeout:
- logger.error(f"通知对方平台超时: {notify_url}")
- except Exception as e:
- logger.error(f"通知对方平台异常: {str(e)}")
- except Exception as e:
- logger.error(f"查询对方平台配置失败: {str(e)}")
-
- return Response({'code': 0, 'msg': '退款成功'})
-
- # ========== 辅助方法 ==========
- def _refund_platform_order(self, order, current_user, tuikuan_liyou, kefu):
- """平台订单退款(调用微信支付)"""
- # 调用微信退款
- refund_result = self._call_wechat_refund(order)
- if refund_result['code'] != 0:
- return Response({'code': 400, 'msg': refund_result['msg']})
-
- with transaction.atomic():
- order.zhuangtai = 5
- order.clkf = current_user.phone
- if tuikuan_liyou:
- order.tkly = tuikuan_liyou
- order.save()
-
- # 更新打手
- dashou_id = order.jiedan_dashou_id
- if dashou_id:
- try:
- dashou_user = UserMain.objects.get(yonghuid=dashou_id)
- dashou_profile = dashou_user.dashou_profile
- dashou_profile.tuikuanliang += 1
- dashou_profile.zhuangtai = 1
- dashou_profile.save()
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"平台退款:打手 {dashou_id} 不存在,跳过更新")
-
- # 更新退款记录
- self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou)
-
- is_cross = order.is_cross
- partner_club_id = order.partner_club_id
- partner_order_id = order.partner_order_id
-
- if not is_cross or not partner_order_id:
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.jiedan_dashou_id,
- amount=order.dashou_fencheng,
- action=3 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
- else:
- pass
-
- # 客服统计
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- logger.info(f"平台订单退款成功: {order.dingdan_id}")
- return Response({'code': 0, 'msg': '退款成功'})
-
- def _refund_merchant_order(self, order, current_user, tuikuan_liyou, kefu):
- """商家订单退款(退余额给商家)"""
- # 获取商家ID
- try:
- shangjia_ext = order.shangjia_kuozhan
- shangjia_id = shangjia_ext.shangjia_id
- except ObjectDoesNotExist:
- return Response({'code': 400, 'msg': '订单商家信息不存在'})
-
- with transaction.atomic():
- # 订单状态改已退款
- order.zhuangtai = 5
- order.clkf = current_user.phone
- if tuikuan_liyou:
- order.tkly = tuikuan_liyou
- order.save()
-
- # 退余额给商家
- try:
- shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
- shangjia_profile = shangjia_user.shop_profile
- shangjia_profile.tuikuan += 1
- shangjia_profile.yue += order.jine or 0
- shangjia_profile.save()
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- logger.error(f"商家退款:商家 {shangjia_id} 不存在,无法退余额")
- raise Exception("商家账户不存在")
-
- # 更新打手
- dashou_id = order.jiedan_dashou_id
- if dashou_id:
- try:
- dashou_user = UserMain.objects.get(yonghuid=dashou_id)
- dashou_profile = dashou_user.dashou_profile
- dashou_profile.tuikuanliang += 1
- dashou_profile.zhuangtai = 1
- dashou_profile.save()
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"商家退款:打手 {dashou_id} 不存在,跳过更新")
-
- # 更新退款记录
- self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou)
-
-
- is_cross = order.is_cross
- partner_club_id = order.partner_club_id
- partner_order_id = order.partner_order_id
-
- if not is_cross or not partner_order_id:
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.jiedan_dashou_id,
- amount=order.dashou_fencheng,
- action=3 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
- else:
- pass
-
- # ✅ 新方法(行为驱动,更安全)
- from houtai.utils import update_shangjia_daily
- update_shangjia_daily(
- yonghuid=shangjia_id,
- amount=Decimal(str(order.jine)),
- action=3 # 3 = 退款
- )
-
- # 客服统计
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- logger.info(f"商家订单退款成功: {order.dingdan_id}")
- return Response({'code': 0, 'msg': '退款成功'})
-
- def _call_wechat_refund(self, order):
- """调用微信支付退款接口,返回 {'code':0, 'msg':'success'} 或错误信息"""
- # 获取微信支付配置
- appid = getattr(settings, 'WEIXIN_APPID', '')
- mch_id = getattr(settings, 'WEIXIN_MCHID', '')
- key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '')
- cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '')
- key_path = getattr(settings, 'WEIXIN_KEY_PATH', '')
-
- if not all([appid, mch_id, key, cert_path, key_path]):
- missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v]
- logger.error(f"微信支付配置缺失: {missing}")
- return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'}
-
- # 生成退款单号
- out_refund_no = self._generate_refund_no(order.dingdan_id)
- total_fee = int(float(order.jine or 0) * 100)
- refund_fee = total_fee
- refund_desc = '客服退款'
-
- params = {
- 'appid': appid,
- 'mch_id': mch_id,
- 'nonce_str': self._generate_nonce_str(),
- 'out_refund_no': out_refund_no,
- 'total_fee': total_fee,
- 'refund_fee': refund_fee,
- 'refund_desc': refund_desc,
- }
- # 优先使用微信交易号
- if order.wechat_transaction_id:
- params['transaction_id'] = order.wechat_transaction_id
- else:
- params['out_trade_no'] = order.dingdan_id
-
- # 签名
- sorted_keys = sorted(params.keys())
- stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys])
- stringSignTemp = f"{stringA}&key={key}"
- sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
- params['sign'] = sign
-
- xml_data = self._dict_to_xml(params)
- url = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
-
- try:
- response = requests.post(url, data=xml_data, cert=(cert_path, key_path), timeout=10)
- result = xmltodict.parse(response.content)['xml']
- logger.info(f"微信退款响应: {result}")
- except Exception as e:
- logger.error(f"微信退款请求异常: {str(e)}", exc_info=True)
- return {'code': 500, 'msg': '微信退款请求异常'}
-
- if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
- return {'code': 0, 'msg': 'success'}
- else:
- err_msg = result.get('err_code_des', result.get('return_msg', '未知错误'))
- logger.warning(f"微信退款失败: {err_msg}")
- return {'code': 400, 'msg': err_msg}
-
- def _refund_to_merchant(self, order, clkf_phone, tuikuan_liyou):
- """给商家退余额(事务内调用,不再独立事务)"""
- try:
- shangjia_ext = order.shangjia_kuozhan
- shangjia_id = shangjia_ext.shangjia_id
- except ObjectDoesNotExist:
- raise Exception("订单商家信息不存在")
-
- try:
- shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
- shangjia_profile = shangjia_user.shop_profile
- shangjia_profile.tuikuan += 1
- shangjia_profile.yue += order.jine or 0
- shangjia_profile.save()
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- logger.error(f"商家退款:商家 {shangjia_id} 不存在")
- raise Exception("商家账户不存在")
-
- def _update_refund_record(self, order, dashou_id, chuli_phone, tuikuan_liyou):
- """更新退款记录表"""
- try:
- #更新支出记录
- if order.fadan_pingtai == 1:
- from dingdan.utils import update_daily_payout
- update_daily_payout(order.jine)
-
- # ========== 新增:商品和店铺每日统计 ==========
- try:
- from shangdian.utils import update_dianpu_daily_stat
- from shangpin.utils import update_shangpin_daily_stat
- # 商品每日统计(只要有商品ID就执行)
- if order.shangpin_id:
- # 从扩展表获取已计算好的收益值(下单时已存储)
- pingtai_shouyi = Decimal('0.00')
- dianpu_shouyi = Decimal('0.00')
- if hasattr(order, 'pingtai_kuozhan'):
- pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00')
- dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
-
- update_shangpin_daily_stat(
- shangpin_id=order.shangpin_id,
- caozuo_leixing=3, # 3 = 退款
- dingdan_jiage=order.jine,
- pingtai_shouyi=pingtai_shouyi,
- dianpu_zongshouyi=dianpu_shouyi
- )
- logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
-
- # 店铺每日统计(仅当存在店铺ID时执行)
- dianpu_id = None
- if hasattr(order, 'pingtai_kuozhan'):
- dianpu_id = order.pingtai_kuozhan.dianpu_id
- if dianpu_id:
- dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
- update_dianpu_daily_stat(
- dianpu_id=dianpu_id,
- caozuo_leixing=3, # 3 = 退款
- dingdan_jiage=order.jine,
- dianpu_shouyi=dianpu_shouyi
- )
- logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
- except Exception as e:
- logger.error(f"商品/店铺统计更新失败: {e}")
- logger.error(traceback.format_exc())
- # 统计失败不影响主流程
- # =============================================
-
-
- refund_record, created = Tuikuanjilu.objects.get_or_create(
- dingdan_id=order.dingdan_id,
- defaults={
- 'dashouid': dashou_id or '',
- 'qingqiuid': '',
- 'chuliid': chuli_phone,
- 'liyou': tuikuan_liyou or '客服退款',
- 'sqzhuangtai': 1,
- 'jine': order.jine or 0,
- 'dashou_fencheng': order.dashou_fencheng or 0,
- 'jieshao': order.jieshao or '',
- 'beizhu': '客服退款',
- 'nicheng': order.nicheng or '',
- }
- )
- if not created:
- refund_record.sqzhuangtai = 1
- refund_record.chuliid = chuli_phone
- if tuikuan_liyou:
- refund_record.liyou = tuikuan_liyou
- refund_record.save()
- except Exception as e:
- logger.error(f"更新退款记录异常: {str(e)}")
-
- def _generate_nonce_str(self):
- return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
-
- def _generate_refund_no(self, dingdan_id):
- timestamp = str(int(time.time()))
- rand = ''.join(random.choices('0123456789', k=4))
- return f"{dingdan_id}REF{timestamp}{rand}"
-
- def _dict_to_xml(self, data):
- xml = ['']
- for k, v in data.items():
- xml.append(f'<{k}>{v}{k}>')
- xml.append('')
- return ''.join(xml)
-
-
-
-
-class PartnerGetOrderDataView(APIView):
- """
- 对方平台调用,获取我方订单的打手图片和处罚记录
- 请求:POST /houtai/kpthqtp
- 参数:{
- "order_id": "对方平台的订单ID",
- "club_id": "对方俱乐部ID"
- }
- 无需认证
- 返回:code=0 + images列表 + punishment信息
- """
- permission_classes = [AllowAny]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # token = request.data.get('token')
- # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN):
- # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200)
- try:
- # 1. 获取参数
- partner_order_id = request.data.get('order_id', '').strip()
- partner_club_id = request.data.get('club_id', '').strip()
- if not partner_order_id or not partner_club_id:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 查询我方存储桶域名(自己俱乐部的配置)
- my_club_config = ClubConfig.objects.filter(is_self=1).first()
- if not my_club_config:
- logger.error("未配置我方俱乐部存储桶域名")
- return Response({'code': 500, 'msg': '系统配置错误'})
- storage_domain = my_club_config.storage_bucket_domain or ''
-
- # 3. 查询我方订单(通过对方订单ID和对方俱乐部ID)
- try:
- order = Dingdan.objects.get(
- dingdan_id=partner_order_id,
- partner_club_id=partner_club_id
- )
- except Dingdan.DoesNotExist:
- logger.warning(f"订单不存在: partner_order_id={partner_order_id}, partner_club_id={partner_club_id}")
- return Response({'code': 404, 'msg': '订单不存在'})
-
- dingdan_id = order.dingdan_id
- dashou_id = order.jiedan_dashou_id
-
- # 4. 查询打手图片(绝对URL)
- images = []
- if dashou_id:
- dashou_images = Dashoutupian.objects.filter(
- dingdan_id=dingdan_id,
- dashou=dashou_id
- ).values_list('tupian', flat=True)
- for rel_path in dashou_images:
- if rel_path:
- if rel_path.startswith('http'):
- images.append(rel_path)
- else:
- images.append(storage_domain + rel_path)
-
- # 5. 查询处罚记录(最新一条)
- punishment = {
- 'cfliyou': '',
- 'sqzhuangtai': 0,
- 'bhliyou': '',
- 'ssliyou': '',
- 'jifen': 0
- }
- if dashou_id:
- punish_record = Chufajilu.objects.filter(
- dingdan_id=dingdan_id,
- dashouid=dashou_id
- ).order_by('-create_time').first()
- if punish_record:
- punishment = {
- 'cfliyou': punish_record.cfliyou or '',
- 'sqzhuangtai': punish_record.sqzhuangtai or 0,
- 'bhliyou': punish_record.bhliyou or '',
- 'ssliyou': punish_record.ssliyou or '',
- 'jifen': punish_record.jifen or 0,
- }
-
- # 6. 返回数据
- return Response({
- 'code': 0,
- 'msg': 'success',
- 'data': {
- 'images': images,
- 'punishment': punishment
- }
- })
-
- except Exception as e:
- logger.error(f"对方获取订单数据接口异常: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-# ========== 新增:对方平台退款通知接口 ==========
-class PartnerRefundNotifyView(APIView):
- """
- 接收对方平台推送的订单状态变更通知(完成/退款),同步更新我方数据。
- 请求:POST /houtai/dfddtk
- 参数:
- {
- "order_id": "对方平台订单ID(实际为我方订单ID)",
- "club_id": "对方俱乐部ID",
- "status": 3 或 5,
- "reason": "理由(可选)"
- }
- 返回:code=0 + data包含我方订单当前状态和打手ID
-
- 核心职责:
- 1. 更新跨平台订单扩展表
- 2. 对于对方派单(我方打手接对方的单):
- a. 状态3(完成)→ 更新订单为已完成,创建预结算(如无)
- b. 状态5(退款)→ 更新订单为已退款,将预结算待结算改为已拒绝
- c. 在此过程中,若订单原状态为“进行中(2)”,则将打手状态重置为空闲(1)
- """
-
- permission_classes = [AllowAny]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # token = request.data.get('token')
- # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN):
- # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200)
- # 1. 参数校验(事务外执行)
- partner_order_id = request.data.get('order_id', '').strip()
- partner_club_id = request.data.get('club_id', '').strip()
- partner_status = request.data.get('status')
- reason = request.data.get('reason', '').strip()
-
- if not partner_order_id or not partner_club_id:
- return Response({'code': 400, 'msg': '参数不完整'})
- if partner_status not in (3, 5):
- return Response({'code': 400, 'msg': '不支持的状态码'})
-
- # 2. 核心业务在事务内执行
- try:
- with transaction.atomic():
- # 2.1 锁定订单行(必须事务内)
- try:
- order = Dingdan.objects.select_for_update().get(
- dingdan_id=partner_order_id,
- partner_club_id=partner_club_id
- )
- except Dingdan.DoesNotExist:
- logger.warning(f"通知订单不存在: order_id={partner_order_id}, club_id={partner_club_id}")
- return Response({'code': 404, 'msg': '订单不存在'})
-
- my_dingdan_id = order.dingdan_id
- dashou_id = order.jiedan_dashou_id or ''
-
- # 记录原订单状态,用于后续打手状态重置
- original_order_status = order.zhuangtai
-
- # 2.2 更新跨平台扩展表(记录对方状态)
- cross_data, created = CrossPlatformOrderData.objects.get_or_create(
- dingdan_id=my_dingdan_id,
- defaults={
- 'partner_order_id': partner_order_id,
- 'partner_club_id': partner_club_id,
- 'partner_order_status': partner_status,
- 'partner_dashou_id': '',
- 'partner_amount': order.jine or 0,
- }
- )
- if not created:
- cross_data.partner_order_status = partner_status
- cross_data.save(update_fields=['partner_order_status', 'update_time'])
-
- # 2.3 仅处理对方派单(dispatch_type == 2)
- if order.dispatch_type == 2:
- # ========== 打手状态恢复逻辑 ==========
- # 如果原订单状态为“进行中(2)”,且存在接单打手,则将打手状态改为空闲(1)
- if original_order_status == 2 and dashou_id:
- try:
- # 通过打手ID查用户主表,再反向获取打手扩展表
- dashou_user = UserMain.objects.get(yonghuid=dashou_id)
- dashou_profile = dashou_user.dashou_profile
- dashou_profile.zhuangtai = 1 # 状态置为空闲
- dashou_profile.save(update_fields=['zhuangtai'])
- logger.info(f"打手 {dashou_id} 状态已重置为1(空闲)")
- except UserMain.DoesNotExist:
- logger.warning(f"打手用户不存在: {dashou_id}")
- except UserDashou.DoesNotExist:
- logger.warning(f"打手扩展信息不存在: {dashou_id}")
- # 🔥 新增:调用打手每日统计(成交)
- if partner_status ==3:
-
- try:
- update_dashou_daily_by_action(
- yonghuid=order.jiedan_dashou_id,
- amount=order.dashou_fencheng,
- action=2 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- if partner_status ==5:
- try:
- update_dashou_daily_by_action(
- yonghuid=order.jiedan_dashou_id,
- amount=order.dashou_fencheng,
- action=3 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
-
- # ========== 情况A:对方完成(status=3) ==========
- if partner_status == 3:
- order.zhuangtai = 3
-
- order.clkf = partner_club_id # 处理人(对方俱乐部ID)
- order.save(update_fields=['zhuangtai', 'update_time','clkf'])
-
- if dashou_id:
- yjs, yjs_created = PreSettlement.objects.get_or_create(
- dingdan_id=my_dingdan_id,
- dashou_id=dashou_id,
- defaults={
- 'partner_order_id': partner_order_id,
- 'partner_club_id': partner_club_id,
- 'amount': order.dashou_fencheng or 0,
- 'order_amount': order.jine or 0,
- 'status': 0, # 待结算
- 'operator': '',
- }
- )
- if yjs_created:
- logger.info(f"预结算创建成功: {my_dingdan_id}, 打手 {dashou_id}, 金额 {order.dashou_fencheng}")
- else:
- logger.info(f"预结算已存在,跳过: {my_dingdan_id}, 打手 {dashou_id}")
- else:
- logger.warning(f"订单 {my_dingdan_id} 无接单打手,跳过预结算创建")
-
- # ========== 情况B:对方退款(status=5) ==========
- elif partner_status == 5:
- order.zhuangtai = 5
- order.tkly = reason if reason else '' # 退款理由
- order.clkf = partner_club_id # 处理人(对方俱乐部ID)
- order.save(update_fields=['zhuangtai', 'update_time','clkf','tkly'])
-
- if dashou_id:
- try:
- yjs = PreSettlement.objects.get(
- dingdan_id=my_dingdan_id,
- dashou_id=dashou_id
- )
- if yjs.status == 0: # 待结算 → 已拒绝
- yjs.status = 2
- yjs.process_time = timezone.now()
- yjs.save(update_fields=['status', 'process_time', 'update_time'])
- logger.info(f"预结算状态已改为已拒绝: {my_dingdan_id}")
- except PreSettlement.DoesNotExist:
- logger.info(f"订单 {my_dingdan_id} 无预结算记录,无需修改")
-
- logger.info(f"对方通知处理完成: dingdan_id={my_dingdan_id}, partner_status={partner_status}")
-
- # 返回结果
- return Response({
- 'code': 0,
- 'msg': 'success',
- 'data': {
- 'partner_status': order.zhuangtai,
- 'partner_dashou_id': dashou_id
- }
- })
-
- except Exception as e:
- logger.error(f"对方通知接口异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-
-
-
-
-from houtai.utils import update_dashou_daily_by_action
-
-class CrossPlatformForceCompleteView(APIView):
- """
- 跨平台订单强制结单接口
- 请求:POST /houtai/kpqzjd
- 参数:{"username": "客服账号ID", "dingdan_id": "订单ID"}
- 认证:JWT
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- username = request.data.get('username', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- if not username or not dingdan_id:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 身份验证
- current_user = request.user
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '003aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权查处理订单'})
-
- # 4. 查询订单并加锁
- try:
- with transaction.atomic():
- order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
-
- # 5. 校验订单状态必须为8(结算中)
- if order.zhuangtai not in (2,8):
- return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'})
-
- # 获取关键字段
- dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单
- is_cross = order.is_cross # 0=本地,1=跨平台
- partner_club_id = order.partner_club_id
- partner_order_id = order.partner_order_id
- dashou_id = order.jiedan_dashou_id
- dashou_fencheng = order.dashou_fencheng or Decimal('0')
- jine = order.jine or Decimal('0')
- fadan_pingtai = order.fadan_pingtai # 1=平台,2=商家
-
- # ========== 修复点1:完善跨平台判断 ==========
- # 跨平台且被对方打手接单的条件:is_cross==1 且 (partner_club_id 或 partner_order_id 有值)
- is_cross_claimed_by_partner = (is_cross == 1 and (partner_club_id or partner_order_id))
- # 如果是跨平台订单但两个字段都为空,则按本地订单处理(即自家打手接单)
-
- # ========== 修复点2:根据方向处理 ==========
- # ---------- 对方派单(我方打手接对方单)----------
- if dispatch_type == 2:
- # 创建预结算记录(如果已存在则更新)
- pre_settlement, created = PreSettlement.objects.get_or_create(
- dingdan_id=dingdan_id,
- defaults={
- 'partner_order_id': partner_order_id,
- 'partner_club_id': partner_club_id,
- 'dashou_id': dashou_id,
- 'amount': dashou_fencheng,
- 'order_amount': jine,
- 'status': 0, # 待结算
- 'operator': current_user.phone,
- }
- )
- if not created:
- pre_settlement.partner_order_id = partner_order_id
- pre_settlement.partner_club_id = partner_club_id
- pre_settlement.dashou_id = dashou_id
- pre_settlement.amount = dashou_fencheng
- pre_settlement.order_amount = jine
- pre_settlement.status = 0
- pre_settlement.process_time = None
- pre_settlement.operator = current_user.phone
- pre_settlement.save()
-
- # 更新订单状态
- order.zhuangtai = 3
- order.clkf = current_user.phone
- order.save()
-
- # 更新每日统计表(对方派单方向,使用订单金额)
- if partner_club_id:
- # 修复点3:使用公共统计函数
- update_daily_dispatch_stat(
- direction=2,
- partner_club_id=partner_club_id,
- success_amount=jine,
- success_count=1
- )
- else:
- # 如果没有俱乐部ID,使用默认000001
- update_daily_dispatch_stat(
- direction=2,
- partner_club_id='000001',
- success_amount=jine,
- success_count=1
- )
-
- # 通知对方平台订单已完成(修复点4:移到事务外)
- if partner_club_id and partner_order_id:
- transaction.on_commit(
- lambda: self._notify_partner_order_status(
- partner_club_id=partner_club_id,
- partner_order_id=partner_order_id,
- new_status=3,
- current_user=current_user
- )
- )
-
- # ---------- 我方派单 ----------
- elif dispatch_type == 1:
- # 判断是否为跨平台订单且被对方打手接单(即 partner_club_id 或 partner_order_id 有值)
- if is_cross_claimed_by_partner:
- # 跨平台订单:我方派单给外平台打手
- # 修复点5:处理只有 partner_order_id 没有 partner_club_id 的情况
- real_partner_club_id = partner_club_id or '000001'
-
- # 更新订单状态
- order.zhuangtai = 3
- order.clkf = current_user.phone
- order.save()
-
- # 更新每日统计表(我方派单方向,使用打手分成金额)
- update_daily_dispatch_stat(
- direction=1,
- partner_club_id=real_partner_club_id,
- success_amount=dashou_fencheng,
- success_count=1
- )
-
-
- # ========== 新增:商品和店铺每日统计 ==========
- if order.fadan_pingtai == 1:
- try:
- from shangdian import update_dianpu_daily_stat
- from shangpin import update_shangpin_daily_stat
- # 商品每日统计(只要有商品ID就执行)
- if order.shangpin_id:
- # 从扩展表获取已计算好的收益值(下单时已存储)
- pingtai_shouyi = Decimal('0.00')
- dianpu_shouyi = Decimal('0.00')
- if hasattr(order, 'pingtai_kuozhan'):
- pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00')
- dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
-
- update_shangpin_daily_stat(
- shangpin_id=order.shangpin_id,
- caozuo_leixing=3, # 3 = 退款
- dingdan_jiage=order.jine,
- pingtai_shouyi=pingtai_shouyi,
- dianpu_zongshouyi=dianpu_shouyi
- )
- logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
-
- # 店铺每日统计(仅当存在店铺ID时执行)
- dianpu_id = None
- if hasattr(order, 'pingtai_kuozhan'):
- dianpu_id = order.pingtai_kuozhan.dianpu_id
- if dianpu_id:
- dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
- update_dianpu_daily_stat(
- dianpu_id=dianpu_id,
- caozuo_leixing=3, # 3 = 退款
- dingdan_jiage=order.jine,
- dianpu_shouyi=dianpu_shouyi
- )
- logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
- except Exception as e:
- logger.error(f"商品/店铺统计更新失败: {e}")
- logger.error(traceback.format_exc())
- # 统计失败不影响主流程
- # =============================================
-
- # 通知对方平台订单已完成(异步)
- if partner_order_id:
- transaction.on_commit(
- lambda: self._notify_partner_order_status(
- partner_club_id=real_partner_club_id,
- partner_order_id=partner_order_id,
- new_status=3,
- current_user=current_user
- )
- )
- else:
- # 本地订单(非跨平台 或 跨平台但实际是自家打手接单),按原有逻辑给自家打手结算
- if not dashou_id:
- return Response({'code': 400, 'msg': '订单无接单打手'})
- if dashou_fencheng <= 0:
- return Response({'code': 400, 'msg': '打手分成金额无效'})
-
- # ========== 新增:商品和店铺每日统计 ==========
- if order.fadan_pingtai == 1:
- try:
- from shangdian.utils import update_dianpu_daily_stat
- from shangpin.utils import update_shangpin_daily_stat
- # 商品每日统计(只要有商品ID就执行)
- if order.shangpin_id:
- # 从扩展表获取已计算好的收益值(下单时已存储)
- pingtai_shouyi = Decimal('0.00')
- dianpu_shouyi = Decimal('0.00')
- if hasattr(order, 'pingtai_kuozhan'):
- pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00')
- dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
-
- update_shangpin_daily_stat(
- shangpin_id=order.shangpin_id,
- caozuo_leixing=3, # 3 = 退款
- dingdan_jiage=order.jine,
- pingtai_shouyi=pingtai_shouyi,
- dianpu_zongshouyi=dianpu_shouyi
- )
- logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
-
- # 店铺每日统计(仅当存在店铺ID时执行)
- dianpu_id = None
- if hasattr(order, 'pingtai_kuozhan'):
- dianpu_id = order.pingtai_kuozhan.dianpu_id
- if dianpu_id:
- dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
- update_dianpu_daily_stat(
- dianpu_id=dianpu_id,
- caozuo_leixing=3, # 3 = 退款
- dingdan_jiage=order.jine,
- dianpu_shouyi=dianpu_shouyi
- )
- logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
- except Exception as e:
- logger.error(f"商品/店铺统计更新失败: {e}")
- logger.error(traceback.format_exc())
- # 统计失败不影响主流程
- # =============================================
-
- # 更新打手余额和统计
- try:
- dashou_user = UserMain.objects.get(yonghuid=dashou_id)
- dashou_profile = dashou_user.dashou_profile
- dashou_profile.chengjiaozongliang += 1
- dashou_profile.yue += dashou_fencheng
- dashou_profile.zonge += dashou_fencheng
- dashou_profile.jinrishouyi += dashou_fencheng
- dashou_profile.jinyueshouyi += dashou_fencheng
- dashou_profile.save()
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"本地订单结算:打手 {dashou_id} 不存在,跳过")
-
- # 如果是商家订单,更新商家成交单量
- if fadan_pingtai == 2:
- try:
- shangjia_ext = order.shangjia_kuozhan
- if shangjia_ext and shangjia_ext.shangjia_id:
- shangjia_user = UserMain.objects.get(yonghuid=shangjia_ext.shangjia_id)
- shangjia_profile = shangjia_user.shop_profile
- shangjia_profile.chengjiao += 1
- shangjia_profile.save()
- except Exception as e:
- logger.warning(f"商家订单更新失败: {str(e)}")
-
- # 更新订单状态
- order.zhuangtai = 3
- order.clkf = current_user.phone
- order.save()
-
- # 本地订单不涉及跨平台统计,无需更新 DailyDispatchStat
-
- else:
- return Response({'code': 400, 'msg': '未知的派单方向'})
-
-
- if dispatch_type == 1 and is_cross_claimed_by_partner:
- pass
- # 判断是否为跨平台订单且被对方打手接单(即 partner_club_id 或 partner_order_id 有值)
- elif dispatch_type == 2:
- pass
- else:
-
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.jiedan_dashou_id,
- amount=dashou_fencheng,
- action=2 # 2 表示成交(结算)
- )
- logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
- if order.fadan_pingtai == 2:
- shangjia_ext = order.shangjia_kuozhan
- # ✅ 新方法(行为驱动,更安全)
- from houtai.utils import update_shangjia_daily
- update_shangjia_daily(
- yonghuid=shangjia_ext.shangjia_id,
- amount=Decimal(str(order.jine)),
- action=2 # 2 = 结算
- )
- else:
- pass
-
- # 7. 更新客服统计
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- # 8. 记录操作日志
- logger.info(f"强制结单成功,订单 {dingdan_id},方向 {dispatch_type},客服 {username}")
-
- return Response({'code': 0, 'msg': '强制结单成功'})
-
- except Dingdan.DoesNotExist:
- logger.warning(f"订单不存在: {dingdan_id}")
- return Response({'code': 404, 'msg': '订单不存在'})
- except Exception as e:
- logger.error(f"强制结单接口异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
- # ========== 辅助方法 ==========
- def _notify_partner_order_status(self, partner_club_id, partner_order_id, new_status, current_user):
- """
- 通知对方平台订单状态变更(已在事务外调用)
- """
- try:
- # 获取我方俱乐部ID
- my_club_config = ClubConfig.objects.filter(is_self=1).first()
- if not my_club_config:
- logger.error("未配置我方俱乐部信息,无法通知对方")
- return
- self_club_id = my_club_config.club_id
-
- # 查询对方服务器域名
- club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
- partner_domain = club_rel.partner_domain
- if not partner_domain:
- logger.error(f"对方俱乐部 {partner_club_id} 未配置域名")
- return
-
- url = partner_domain.rstrip('/') + '/houtai/dfddtk'
- payload = {
- 'order_id': partner_order_id,
- 'club_id': self_club_id,
- 'status': new_status
- }
- resp = requests.post(url, json=payload, timeout=3)
- if resp.status_code == 200:
- resp_json = resp.json()
- if resp_json.get('code') == 0:
- # 更新跨平台订单扩展表中的对方状态
- cross_data, _ = CrossPlatformOrderData.objects.get_or_create(
- dingdan_id=partner_order_id,
- defaults={
- 'partner_order_id': partner_order_id,
- 'partner_club_id': partner_club_id,
- 'partner_order_status': new_status,
- 'partner_dashou_id': '',
- 'partner_amount': Decimal('0')
- }
- )
- if not _:
- cross_data.partner_order_status = new_status
- cross_data.save(update_fields=['partner_order_status'])
- logger.info(f"通知对方订单状态成功: {partner_order_id} -> {new_status}")
- else:
- logger.warning(f"对方接口返回错误: {resp_json.get('msg')}")
- else:
- logger.warning(f"对方接口HTTP错误: {resp.status_code}")
- except requests.exceptions.Timeout:
- logger.error(f"通知对方平台超时: {partner_club_id}")
- except Exception as e:
- logger.error(f"通知对方平台异常: {str(e)}")
-
-
-
-
-class CrossPlatformTransferHallView(APIView):
- """
- 跨平台订单转移大厅接口
- 请求:POST /houtai/kpzydt
- 参数:{"username": "客服账号ID", "dingdan_id": "订单ID"}
- 认证:JWT
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- username = request.data.get('username', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- if not username or not dingdan_id:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 客服身份验证
- current_user = request.user
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '003aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权处理俱乐部订单'})
-
- # 4. 查询订单并加锁
- try:
- with transaction.atomic():
- order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
-
- # 5. 状态校验
- if order.zhuangtai not in [2, 8, 4]:
- return Response({'code': 400, 'msg': f'订单状态不允许转移大厅,当前状态: {order.zhuangtai}'})
-
- # 获取关键字段
- is_cross = order.is_cross
- dispatch_type = order.dispatch_type
- partner_club_id = order.partner_club_id
- partner_order_id = order.partner_order_id
- current_dashou_id = order.jiedan_dashou_id
- zhiding_id = order.zhiding_id
-
- # ========== 判断是否需要通知对方平台 ==========
- # 条件:跨平台 且 我方派单(dispatch_type=1) 且 (partner_club_id 或 partner_order_id 有值)
- need_notify = (is_cross == 1 and (partner_club_id or partner_order_id))
-
- # ========== 判断是否需要修改本地打手状态 ==========
- # 当接单打手是我方打手时,才需要修改状态(改为空闲1)
- # 我方打手的情况:
- # 1. 非跨平台订单 (is_cross != 1)
- # 2. 跨平台且对方派单 (dispatch_type == 2) —— 此时打手是我方
- # 3. 跨平台且我方派单但没有对方信息 (即 need_notify == False) —— 此时打手也是我方
- need_update_dashou_status = (current_dashou_id and
- (is_cross != 1 or dispatch_type == 2 or not need_notify))
-
- # ========== 记录打手历史(无论打手是谁,只要有ID就记录) ==========
- if current_dashou_id:
- last_history = OrderDashouHistory.objects.filter(
- dingdan_id=dingdan_id
- ).order_by('-times').first()
- next_times = (last_history.times + 1) if last_history else 1
- OrderDashouHistory.objects.create(
- dingdan_id=dingdan_id,
- dashou_id=current_dashou_id,
- times=next_times,
- operator=username
- )
-
- # ========== 修改本地打手状态(仅当是我方打手时) ==========
- if need_update_dashou_status:
- try:
- dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id)
- dashou_profile.zhuangtai = 1
- dashou_profile.save()
- logger.info(f"打手 {current_dashou_id} 状态改为空闲")
- except ObjectDoesNotExist:
- logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新")
-
- # ========== 更新订单状态(清空打手,改为1或7) ==========
- if zhiding_id:
- order.zhuangtai = 7 # 指定中
- else:
- order.zhuangtai = 1 # 下单中
- order.jiedan_dashou_id = None
- order.clkf = username
- order.save()
-
- # ========== 如果需要通知对方平台,异步发送通知 ==========
- if need_notify:
- # 确定对方俱乐部ID(如果没有则用默认000001)
- real_partner_club_id = partner_club_id if partner_club_id else '000001'
- real_partner_order_id = partner_order_id if partner_order_id else dingdan_id
-
- # 获取我方俱乐部ID
- my_club_config = ClubConfig.objects.filter(is_self=1).first()
- if my_club_config:
- self_club_id = my_club_config.club_id
- # 查询对方域名
- try:
- club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=real_partner_club_id)
- partner_domain = club_rel.partner_domain
- if partner_domain:
- url = partner_domain.rstrip('/') + '/houtai/kpttzzydt'
- payload = {
- 'order_id': real_partner_order_id,
- 'club_id': self_club_id
- }
-
- def notify():
- try:
- resp = requests.post(url, json=payload, timeout=3)
- if resp.status_code == 200:
- resp_json = resp.json()
- if resp_json.get('code') == 0:
- # 更新跨平台订单扩展表中的对方状态
- cross_data, created = CrossPlatformOrderData.objects.get_or_create(
- dingdan_id=dingdan_id,
- defaults={
- 'partner_order_id': real_partner_order_id,
- 'partner_club_id': real_partner_club_id,
- 'partner_order_status': resp_json.get('data', {}).get('partner_status', 0),
- 'partner_dashou_id': '',
- 'partner_amount': order.jine or 0,
- }
- )
- if not created:
- cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0)
- cross_data.save(update_fields=['partner_order_status'])
- logger.info(f"转移大厅通知对方成功: {dingdan_id}")
-
- # ========== 新增代码开始 ==========
- # 对方成功转移大厅,清空我方订单中的对方相关信息
- # ========== 新增代码开始 ==========
- # 只有我方派单时才清空对方平台信息,对方派单则保留
- if dispatch_type == 1:
- Dingdan.objects.filter(dingdan_id=dingdan_id).update(
- partner_club_id=None,
- partner_order_id=None,
- jiedan_dashou_id=None,
- zhuangtai=1
- )
- logger.info(f"我方派单,已清空订单 {dingdan_id} 的对方信息")
- else:
- # 对方派单,只清空打手并恢复状态,保留对方信息
- Dingdan.objects.filter(dingdan_id=dingdan_id).update(
- jiedan_dashou_id=None,
- zhuangtai=1
- )
- logger.info(f"对方派单,只清空打手,保留对方信息,订单 {dingdan_id}")
- # ========== 新增代码结束 ==========
- # ========== 新增代码结束 ==========
-
- else:
- logger.warning(f"对方平台返回错误: {resp_json.get('msg')}")
- else:
- logger.warning(f"对方接口HTTP错误: {resp.status_code}")
- except Exception as e:
- logger.error(f"转移大厅通知对方异常: {str(e)}")
-
- threading.Thread(target=notify).start()
- except Club.DoesNotExist:
- logger.error(f"未找到对方俱乐部配置: partner_club_id={real_partner_club_id}")
- except Exception as e:
- logger.error(f"查询对方平台配置失败: {str(e)}")
-
- # ========== 更新客服统计 ==========
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- return Response({'code': 0, 'msg': '转移大厅成功'})
-
- except Dingdan.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'})
- except Exception as e:
- logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-class PartnerTransferHallNotifyView(APIView):
- """
- 对方平台转移大厅通知接口
- 对方调用此接口通知我方订单已转移大厅,我方同步更新订单状态
- 请求:POST /houtai/kpttzzydt
- 参数:{
- "order_id": "对方平台的订单ID",
- "club_id": "对方俱乐部ID"
- }
- 无需认证
- 返回:{
- "code": 0,
- "msg": "success",
- "data": {
- "partner_status": 1 # 我方订单当前状态
- }
- }
- """
- parser_classes = [JSONParser]
- permission_classes = [AllowAny]
-
- def post(self, request):
- # token = request.data.get('token')
- # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN):
- # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200)
- try:
- # 1. 获取参数
- partner_order_id = request.data.get('order_id', '').strip()
- partner_club_id = request.data.get('club_id', '').strip()
- if not partner_order_id or not partner_club_id:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 查询我方订单
- try:
- order = Dingdan.objects.get(
- dingdan_id=partner_order_id,
- partner_club_id=partner_club_id
- )
- except Dingdan.DoesNotExist:
- logger.warning(f"对方转移大厅通知:订单不存在 partner_order_id={partner_order_id}, partner_club_id={partner_club_id}")
- return Response({'code': 404, 'msg': '订单不存在'})
-
- # 3. 加锁处理
- with transaction.atomic():
- order = Dingdan.objects.select_for_update().get(id=order.id)
-
- current_status = order.zhuangtai
- current_dashou_id = order.jiedan_dashou_id
-
- # 4. 根据订单状态处理
- if current_status == 3:
- # 已完成状态,需要检查预结算记录
- try:
- pre_settlement = PreSettlement.objects.select_for_update().get(
- dingdan_id=order.dingdan_id,
- dashou_id=current_dashou_id
- )
- # 如果预结算状态是0(待结算),改为2(已拒绝)
- if pre_settlement.status == 0:
- pre_settlement.status = 2
- pre_settlement.process_time = timezone.now()
- pre_settlement.operator = partner_club_id # 记录操作方
- pre_settlement.save()
- logger.info(f"预结算记录已拒绝: {order.dingdan_id}")
- else:
- logger.info(f"预结算记录已是其他状态 {pre_settlement.status},不重复修改")
- except PreSettlement.DoesNotExist:
- logger.warning(f"订单 {order.dingdan_id} 已完成但无预结算记录,无法转移大厅")
- return Response({'code': 400, 'msg': '订单已完成且无预结算记录,无法转移大厅'})
-
- # 5. 记录打手历史(如果有接单打手)
- if current_dashou_id:
- last_history = OrderDashouHistory.objects.filter(
- dingdan_id=order.dingdan_id
- ).order_by('-times').first()
- next_times = (last_history.times + 1) if last_history else 1
- OrderDashouHistory.objects.create(
- dingdan_id=order.dingdan_id,
- dashou_id=current_dashou_id,
- times=next_times,
- operator=partner_club_id # 记录操作方
- )
- # 将打手状态改为空闲
- try:
- dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id)
- dashou_profile.zhuangtai = 1
- dashou_profile.save()
-
- # 打手退款每日统计
- if order.dispatch_type == 2:
- try:
- update_dashou_daily_by_action(
- yonghuid=current_dashou_id,
- amount=dashou_profile.dashou_fencheng,
- action=3 # 退款
- )
- except Exception as e:
- logger.error(f"记录打手统计错误: {str(e)}")
- except ObjectDoesNotExist:
- logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新")
-
- # 6. 更新订单状态为1(下单中)或7(指定中)
- # 7. 根据派单方向清理对方信息
- if order.dispatch_type == 1:
- # 我方派单:清空对方俱乐部ID和订单ID
- order.partner_club_id = None
- order.partner_order_id = None
- # 对方派单时保留对方信息,不清空
-
- # 8. 更新订单状态
- if order.zhiding_id:
- order.zhuangtai = 7 # 指定中
- else:
- order.zhuangtai = 1 # 下单中
-
-
- order.jiedan_dashou_id = None
- order.clkf = partner_club_id # 记录操作方
- order.save()
-
- logger.info(f"对方转移大厅通知成功: 订单 {order.dingdan_id},新状态 {order.zhuangtai}")
-
- # 7. 返回我方订单状态
- return Response({
- 'code': 0,
- 'msg': 'success',
- 'data': {
- 'partner_status': order.zhuangtai
- }
- })
-
- except Exception as e:
- logger.error(f"对方转移大厅通知接口异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-
-
-
-
-logger = logging.getLogger(__name__)
-
-
-# ========== 1. 获取俱乐部设置数据接口 ==========
-class GetSettingsView(APIView):
- """
- 获取跨平台设置页面数据
- 请求:POST /houtai/hqkptsz
- 参数:{"username": "客服账号ID"}
- 权限:权限码2且开启
- 返回:包含自己俱乐部信息(含客服字段)和合作俱乐部列表
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 客服身份验证
- current_user = request.user
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '004aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
-
- # 4. 查询数据
- try:
- # 4.1 自己俱乐部配置(is_self=1)
- self_config = ClubConfig.objects.filter(is_self=1).first()
- if not self_config:
- logger.error("未找到自己俱乐部配置")
- return Response({'code': 500, 'msg': '系统配置错误'})
-
- self_club = {
- 'club_id': self_config.club_id,
- 'club_nickname': self_config.club_nickname,
- 'club_avatar': self_config.club_avatar,
- 'backend_domain': self_config.backend_domain,
- 'chat_api_key': self_config.chat_api_key,
- 'chat_api_id': self_config.chat_api_id,
- 'customer_service_api_url': self_config.customer_service_api_url,
- 'customer_service_corp_id': self_config.customer_service_corp_id,
- 'storage_bucket_domain': self_config.storage_bucket_domain,
- }
-
- # 4.2 合作俱乐部列表(Club 表中本俱乐部ID = self_config.club_id)
- partner_records = Club.objects.filter(club_id=self_config.club_id)
- partner_clubs = []
- for record in partner_records:
- partner_config = ClubConfig.objects.filter(
- club_id=record.partner_club_id,
- is_self=0
- ).first()
- partner_clubs.append({
- 'club_id': record.partner_club_id,
- 'club_nickname': partner_config.club_nickname if partner_config else record.partner_club_id,
- 'club_avatar': partner_config.club_avatar if partner_config else '',
- 'is_interop_enabled': record.is_interop_enabled,
- 'is_receive_enabled': record.is_receive_enabled,
- })
-
- return Response({'code': 0, 'data': {'self_club': self_club, 'partner_clubs': partner_clubs}})
-
- except Exception as e:
- logger.error(f"获取设置失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-# ========== 2. 获取订单类型映射接口 ==========
-class GetMappingsView(APIView):
- """
- 获取指定合作俱乐部的订单类型映射
- 请求:POST /houtai/hqqtjlbsj
- 参数:{"username": "客服账号ID", "partner_club_id": "对方俱乐部ID"}
- 权限:权限码2
- 返回:对方订单类型列表、我方订单类型列表、当前映射关系
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- username = request.data.get('username', '').strip()
- partner_club_id = request.data.get('partner_club_id', '').strip()
- if not username or not partner_club_id:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 客服身份验证
- current_user = request.user
-
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '004aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
-
- try:
- # 4. 获取我方所有商品类型(订单类型)
- our_types = ShangpinLeixing.objects.all().values('id', 'jieshao', 'tupian_url')
- our_type_list = [
- {'type_id': t['id'], 'type_name': t['jieshao'], 'icon': t['tupian_url']}
- for t in our_types
- ]
-
- # 5. 获取对方订单类型(从映射表去重)
- mappings_query = OrderTypeMapping.objects.filter(partner_club_id=partner_club_id)
- partner_types_dict = {}
- for m in mappings_query:
- partner_types_dict[m.partner_type_id] = {
- 'type_id': m.partner_type_id,
- 'type_name': m.partner_type_name,
- 'icon': ''
- }
- partner_type_list = list(partner_types_dict.values())
-
- # 6. 获取当前映射关系
- mapping_list = [
- {'partner_type_id': m.partner_type_id, 'our_type_id': m.our_type_id}
- for m in mappings_query
- ]
-
- return Response({
- 'code': 0,
- 'data': {
- 'partner_types': partner_type_list,
- 'our_types': our_type_list,
- 'mappings': mapping_list
- }
- })
-
- except Exception as e:
- logger.error(f"获取映射失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-# ========== 3. 保存设置接口(自己俱乐部 + 合作俱乐部开关)==========
-class UpdateSettingsView(APIView):
- """
- 保存俱乐部设置(自己俱乐部信息 + 合作俱乐部开关)
- 请求:POST /houtai/xgjlbsj
- 参数:{
- "username": "客服账号ID",
- "self_club": {
- "club_nickname": "...",
- "backend_domain": "...",
- "chat_api_key": "...",
- "chat_api_id": "...",
- "customer_service_api_url": "...",
- "customer_service_corp_id": "...",
- "storage_bucket_domain": "..."
- },
- "partner_updates": [
- {"club_id": "654321", "is_interop_enabled": true, "is_receive_enabled": false}
- ]
- }
- 权限:权限码2
- 返回:{"code": 0, "msg": "success"}
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 客服身份验证
- current_user = request.user
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '004aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权修改俱乐部配置'})
-
- # 4. 获取自己俱乐部当前配置
- try:
- self_config = ClubConfig.objects.get(is_self=1)
- except ObjectDoesNotExist:
- logger.error("未找到自己俱乐部配置")
- return Response({'code': 500, 'msg': '系统配置错误'})
-
- # 5. 更新自己俱乐部配置(包含客服字段)
- self_updates = request.data.get('self_club', {})
- if self_updates:
- allowed_fields = [
- 'club_nickname', 'backend_domain', 'chat_api_key', 'chat_api_id',
- 'customer_service_api_url', 'customer_service_corp_id',
- 'storage_bucket_domain'
- ]
- fields_to_update = []
- for field in allowed_fields:
- if field in self_updates:
- setattr(self_config, field, self_updates[field])
- fields_to_update.append(field)
- if fields_to_update:
- self_config.save(update_fields=fields_to_update)
-
- # 6. 更新合作俱乐部开关
- partner_updates = request.data.get('partner_updates', [])
- for update in partner_updates:
- club_id = update.get('club_id')
- if not club_id:
- continue
- try:
- partner_record = Club.objects.get(club_id=self_config.club_id, partner_club_id=club_id)
- if 'is_interop_enabled' in update:
- partner_record.is_interop_enabled = update['is_interop_enabled']
- if 'is_receive_enabled' in update:
- partner_record.is_receive_enabled = update['is_receive_enabled']
- partner_record.save(update_fields=['is_interop_enabled', 'is_receive_enabled'])
- except ObjectDoesNotExist:
- logger.warning(f"合作俱乐部记录不存在: {club_id}")
-
- # 7. 异步通知所有合作俱乐部(如果自己数据有修改)
- if self_updates:
- self._notify_all_partners(self_config, self_updates)
-
- return Response({'code': 0, 'msg': 'success'})
-
- def _notify_all_partners(self, self_config, updates):
- """异步通知所有合作俱乐部更新我方配置"""
- partner_records = Club.objects.filter(club_id=self_config.club_id)
- if not partner_records:
- return
-
- data_to_send = {
- 'club_id': self_config.club_id,
- 'updates': updates
- }
-
- def notify(partner_domain):
- try:
- url = partner_domain.rstrip('/') + '/houtai/xgzjsjcs'
- resp = requests.post(url, json=data_to_send, timeout=3)
- if resp.status_code != 200:
- logger.warning(f"通知 {partner_domain} 失败,状态码 {resp.status_code}")
- except Exception as e:
- logger.error(f"通知 {partner_domain} 异常: {str(e)}")
-
- for record in partner_records:
- domain = record.partner_domain
- if domain:
- threading.Thread(target=notify, args=(domain,)).start()
-
-
-
-
-
-class UpdateMappingsView(APIView):
- """
- 保存指定合作俱乐部的订单类型映射
- 请求:POST /houtai/xgdfddys
- 参数:{
- "username": "客服手机号",
- "partner_club_id": "对方俱乐部ID",
- "mappings": [
- {"partner_type_id": 1, "our_type_id": 10},
- {"partner_type_id": 2, "our_type_id": 11}
- ]
- }
- 权限:权限码2
- 返回:{"code": 0, "msg": "success"}
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- username = request.data.get('username', '').strip()
- partner_club_id = request.data.get('partner_club_id', '').strip()
- mappings = request.data.get('mappings', [])
-
- if not username or not partner_club_id:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 客服身份验证
- current_user = request.user
-
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '004aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权修改俱乐部配置'})
-
- try:
- with transaction.atomic():
- # 4. 获取当前该俱乐部所有现有映射(按 partner_type_id 索引)
- existing_mappings = {
- m.partner_type_id: m
- for m in OrderTypeMapping.objects.filter(partner_club_id=partner_club_id)
- }
-
- # 5. 处理前端传来的映射(更新或新增)
- processed_partner_ids = set()
- for mapping in mappings:
- partner_type_id = mapping.get('partner_type_id')
- our_type_id = mapping.get('our_type_id')
- if partner_type_id is None or our_type_id is None:
- continue
- processed_partner_ids.add(partner_type_id)
-
- # 获取我方类型名称
- our_type_name = ''
- try:
- our_type = ShangpinLeixing.objects.get(id=our_type_id)
- our_type_name = our_type.jieshao
- except ObjectDoesNotExist:
- logger.warning(f"我方订单类型不存在: {our_type_id}")
-
- # 查找是否存在该对方类型的映射
- if partner_type_id in existing_mappings:
- # 更新现有记录
- mapping_obj = existing_mappings[partner_type_id]
- mapping_obj.our_type_id = our_type_id
- mapping_obj.our_type_name = our_type_name
- mapping_obj.save(update_fields=['our_type_id', 'our_type_name'])
- # 从字典中移除,以便后续知道哪些被删除了
- del existing_mappings[partner_type_id]
- else:
- # 新增记录
- # 对方类型名称:优先从现有映射中查找(可能被其他俱乐部共享?不,每个俱乐部独立)
- # 由于该对方类型没有映射过,我们暂时无法获取名称,可留空或使用默认值。
- # 但为保证数据完整性,我们尝试从已有记录中查找(如果该俱乐部之前删除过又重建,则可能丢失名称)
- # 更稳妥的方式是前端传递 partner_type_name,但当前未要求,因此此处留空并记录警告。
- partner_type_name = ''
- logger.warning(f"新增映射时对方类型名称缺失: partner_club_id={partner_club_id}, partner_type_id={partner_type_id}")
-
- OrderTypeMapping.objects.create(
- partner_club_id=partner_club_id,
- partner_type_id=partner_type_id,
- partner_type_name=partner_type_name,
- our_type_id=our_type_id,
- our_type_name=our_type_name
- )
-
- # 6. 删除前端未传入的映射(即用户取消的映射)
- for partner_type_id, mapping_obj in existing_mappings.items():
- mapping_obj.delete()
- logger.info(f"删除映射: partner_club_id={partner_club_id}, partner_type_id={partner_type_id}")
-
- return Response({'code': 0, 'msg': 'success'})
-
- except Exception as e:
- logger.error(f"保存映射失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-# ========== 5. 接收对方平台修改数据接口 ==========
-class PartnerUpdateSelfConfigView(APIView):
- """
- 接收对方平台修改自己俱乐部配置的通知
- 请求:POST /houtai/xgzjsjcs
- 参数:{
- "club_id": "对方俱乐部ID",
- "updates": {
- "club_nickname": "新昵称",
- "backend_domain": "新域名",
- "chat_api_key": "新key",
- "chat_api_id": "新id",
- "customer_service_api_url": "新URL",
- "customer_service_corp_id": "新企业ID",
- "storage_bucket_domain": "新存储桶"
- }
- }
- 无需认证,对方平台可信调用
- 返回:{"code": 0, "msg": "success"}
- """
- parser_classes = [JSONParser]
-
- def post(self, request):
- # token = request.data.get('token')
- # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN):
- # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200)
- # 1. 获取参数
- club_id = request.data.get('club_id', '').strip()
- updates = request.data.get('updates', {})
-
- if not club_id or not updates:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 查询对方俱乐部的配置记录(is_self=0)
- try:
- config = ClubConfig.objects.get(club_id=club_id, is_self=0)
- except ClubConfig.DoesNotExist:
- logger.warning(f"对方俱乐部配置不存在: {club_id}")
- return Response({'code': 404, 'msg': '俱乐部不存在'})
-
- # 3. 允许修改的字段白名单(包含客服字段)
- allowed_fields = [
- 'club_nickname',
- 'backend_domain',
- 'chat_api_key',
- 'chat_api_id',
- 'customer_service_api_url',
- 'customer_service_corp_id',
- 'storage_bucket_domain'
- ]
-
- # 4. 更新字段
- with transaction.atomic():
- fields_updated = []
- for field in allowed_fields:
- if field in updates:
- new_value = updates[field]
- setattr(config, field, new_value)
- fields_updated.append(field)
- if fields_updated:
- config.save(update_fields=fields_updated)
- logger.info(f"对方俱乐部 {club_id} 配置已更新: {fields_updated}")
- else:
- logger.info(f"对方俱乐部 {club_id} 无有效字段更新")
-
- return Response({'code': 0, 'msg': 'success'})
-
-
-
-
-# apps/houtai/views.py
-
-
-
-
-
-# ========== 1. 获取合作俱乐部列表接口(完整) ==========
-class GetPartnerClubsView(APIView):
- """
- 获取我方合作俱乐部列表(用于统计页面)
- 请求:POST /houtai/kpthqpt
- 参数:{"username": "客服手机号"}
- 权限:权限码2且开启
- 返回:{"code":0, "data":[{"club_id":"xxx","club_nickname":"xxx","club_avatar":"xxx"}]}
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 客服身份验证
- current_user = request.user
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '004bb' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
-
- # 4. 获取我方俱乐部ID
- try:
- self_config = ClubConfig.objects.get(is_self=1)
- except ObjectDoesNotExist:
- logger.error("未找到自己俱乐部配置")
- return Response({'code': 500, 'msg': '系统配置错误'})
-
- # 5. 查询所有合作俱乐部
- try:
- records = Club.objects.filter(club_id=self_config.club_id)
- result = []
- for record in records:
- partner_config = ClubConfig.objects.filter(
- club_id=record.partner_club_id,
- is_self=0
- ).first()
- result.append({
- 'club_id': record.partner_club_id,
- 'club_nickname': partner_config.club_nickname if partner_config else record.partner_club_id,
- 'club_avatar': partner_config.club_avatar if partner_config else '',
- })
- return Response({'code': 0, 'data': result})
- except Exception as e:
- logger.error(f"查询合作俱乐部失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-# ========== 2. 获取统计数据接口(完整) ==========
-
-
-
-
-
-class GetStatsView(APIView):
- """
- 获取派单统计数据(按日或按月)
- 请求:POST /houtai/hqpfsj
- 参数:{
- "username": "客服手机号",
- "club_id": "对方俱乐部ID",
- "direction": 1, # 1=我方派单,2=对方派单
- "granularity": "day", # "day" 或 "month"
- # 趋势图参数(可选,不传则使用默认值):
- "trend_year_month": "YYYY-MM", # 按日时表示月份,缺省当前月
- "trend_year": "YYYY", # 按月时表示年份,缺省当前年
- # KPI参数(可选,不传则使用默认值):
- "date": "YYYY-MM-DD", # 按日时表示日期,缺省当天
- "year_month": "YYYY-MM" # 按月时表示月份,缺省当前月
- }
- 权限:权限码2
- 返回:{
- "code": 0,
- "data": {
- "summary": { ... },
- "chart": { ... }
- }
- }
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取基础参数
- username = request.data.get('username', '').strip()
- club_id = request.data.get('club_id', '').strip()
- direction = request.data.get('direction')
- granularity = request.data.get('granularity')
-
- # 基础参数校验
- if not username:
- return Response({'code': 400, 'msg': 'username 不能为空'})
- if not club_id:
- return Response({'code': 400, 'msg': 'club_id 不能为空'})
- if direction is None:
- return Response({'code': 400, 'msg': 'direction 不能为空'})
- if granularity not in ('day', 'month'):
- return Response({'code': 400, 'msg': 'granularity 必须是 day 或 month'})
- try:
- direction = int(direction)
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': 'direction 必须为整数'})
-
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '004bb' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
-
- # 4. 获取当前时间用于默认值
- now = datetime.now()
-
- # 5. 处理趋势图参数(默认当前月/年)
- if granularity == 'day':
- trend_year_month = request.data.get('trend_year_month')
- if not trend_year_month:
- trend_year_month = now.strftime('%Y-%m')
- try:
- year, month = map(int, trend_year_month.split('-'))
- start_date = datetime(year, month, 1).date()
- _, days_in_month = monthrange(year, month)
- end_date = datetime(year, month, days_in_month).date()
- except (ValueError, TypeError):
- # 如果格式错误,回退到当前月
- year, month = now.year, now.month
- start_date = datetime(year, month, 1).date()
- _, days_in_month = monthrange(year, month)
- end_date = datetime(year, month, days_in_month).date()
- else:
- trend_year = request.data.get('trend_year')
- if not trend_year:
- trend_year = str(now.year)
- try:
- year = int(trend_year)
- except (ValueError, TypeError):
- year = now.year
- start_date = datetime(year, 1, 1).date()
- end_date = datetime(year, 12, 31).date()
-
- # 6. 处理KPI参数(默认当天/当月)
- if granularity == 'day':
- date_str = request.data.get('date')
- if not date_str:
- date_str = now.strftime('%Y-%m-%d')
- try:
- target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
- except (ValueError, TypeError):
- target_date = now.date()
- else:
- year_month_str = request.data.get('year_month')
- if not year_month_str:
- year_month_str = now.strftime('%Y-%m')
- try:
- year, month = map(int, year_month_str.split('-'))
- start_date_kpi = datetime(year, month, 1).date()
- _, days_in_month = monthrange(year, month)
- end_date_kpi = datetime(year, month, days_in_month).date()
- except (ValueError, TypeError):
- year, month = now.year, now.month
- start_date_kpi = datetime(year, month, 1).date()
- _, days_in_month = monthrange(year, month)
- end_date_kpi = datetime(year, month, days_in_month).date()
-
- # 7. 查询趋势图数据
- response_data = {
- 'summary': {'dispatch_count': 0, 'dispatch_amount': 0.0, 'success_count': 0, 'success_amount': 0.0},
- 'chart': {'labels': [], 'dispatch_counts': [], 'dispatch_amounts': [], 'success_counts': [], 'success_amounts': []}
- }
-
- try:
- if granularity == 'day':
- qs = DailyDispatchStat.objects.filter(
- partner_club_id=club_id,
- direction=direction,
- date__gte=start_date,
- date__lte=end_date
- ).order_by('date')
- data_dict = {}
- for item in qs:
- data_dict[item.date] = {
- 'dispatch_count': item.dispatch_count,
- 'dispatch_amount': float(item.dispatch_amount),
- 'success_count': item.success_count,
- 'success_amount': float(item.success_amount),
- }
- labels = []
- dispatch_counts = []
- dispatch_amounts = []
- success_counts = []
- success_amounts = []
- current = start_date
- while current <= end_date:
- labels.append(current.strftime('%d'))
- day_data = data_dict.get(current, {})
- dispatch_counts.append(day_data.get('dispatch_count', 0))
- dispatch_amounts.append(day_data.get('dispatch_amount', 0.0))
- success_counts.append(day_data.get('success_count', 0))
- success_amounts.append(day_data.get('success_amount', 0.0))
- current += timedelta(days=1)
- response_data['chart'] = {
- 'labels': labels,
- 'dispatch_counts': dispatch_counts,
- 'dispatch_amounts': dispatch_amounts,
- 'success_counts': success_counts,
- 'success_amounts': success_amounts,
- }
- else: # month
- qs = DailyDispatchStat.objects.filter(
- partner_club_id=club_id,
- direction=direction,
- date__gte=start_date,
- date__lte=end_date
- ).order_by('date')
- month_totals = {m: {'dispatch_count': 0, 'dispatch_amount': 0.0, 'success_count': 0, 'success_amount': 0.0}
- for m in range(1, 13)}
- for item in qs:
- m = item.date.month
- month_totals[m]['dispatch_count'] += item.dispatch_count
- month_totals[m]['dispatch_amount'] += float(item.dispatch_amount)
- month_totals[m]['success_count'] += item.success_count
- month_totals[m]['success_amount'] += float(item.success_amount)
- labels = []
- dispatch_counts = []
- dispatch_amounts = []
- success_counts = []
- success_amounts = []
- for m in range(1, 13):
- labels.append(f"{m}月")
- dispatch_counts.append(month_totals[m]['dispatch_count'])
- dispatch_amounts.append(month_totals[m]['dispatch_amount'])
- success_counts.append(month_totals[m]['success_count'])
- success_amounts.append(month_totals[m]['success_amount'])
- response_data['chart'] = {
- 'labels': labels,
- 'dispatch_counts': dispatch_counts,
- 'dispatch_amounts': dispatch_amounts,
- 'success_counts': success_counts,
- 'success_amounts': success_amounts,
- }
-
- # 8. 查询KPI数据
- if granularity == 'day':
- try:
- stat = DailyDispatchStat.objects.get(
- partner_club_id=club_id,
- direction=direction,
- date=target_date
- )
- response_data['summary'] = {
- 'dispatch_count': stat.dispatch_count,
- 'dispatch_amount': float(stat.dispatch_amount),
- 'success_count': stat.success_count,
- 'success_amount': float(stat.success_amount),
- }
- except DailyDispatchStat.DoesNotExist:
- response_data['summary'] = {
- 'dispatch_count': 0,
- 'dispatch_amount': 0.0,
- 'success_count': 0,
- 'success_amount': 0.0,
- }
- else: # month
- qs_kpi = DailyDispatchStat.objects.filter(
- partner_club_id=club_id,
- direction=direction,
- date__gte=start_date_kpi,
- date__lte=end_date_kpi
- )
- summary = qs_kpi.aggregate(
- dispatch_count=Sum('dispatch_count'),
- dispatch_amount=Sum('dispatch_amount'),
- success_count=Sum('success_count'),
- success_amount=Sum('success_amount')
- )
- response_data['summary'] = {
- 'dispatch_count': summary['dispatch_count'] or 0,
- 'dispatch_amount': float(summary['dispatch_amount'] or 0.0),
- 'success_count': summary['success_count'] or 0,
- 'success_amount': float(summary['success_amount'] or 0.0),
- }
-
- return Response({'code': 0, 'data': response_data})
-
- except Exception as e:
- logger.error(f"获取统计数据失败: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-
-
-class PunishDashouView(APIView):
- """
- 客服处罚打手接口
- 请求:POST /houtai/kpcf
- 参数:{
- "username": "客服手机号",
- "dingdan_id": "订单ID",
- "reason": "处罚原因(可选)"
- }
- 认证:JWT,权限码1
- 返回:code=0 + 处罚结果
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- start_time = time.time()
-
- # 1. 获取参数
- username = request.data.get('username', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- reason = request.data.get('reason', '').strip()
-
- if not username or not dingdan_id:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- try:
- # 2. 身份验证
- current_user = request.user
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '003aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权处罚'})
-
- # 4. 查询订单并加锁
- with transaction.atomic():
- order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
-
- # 5. 订单状态校验(允许处罚的状态:1,2,3,4,5,6,7,8,即除已完成以外的状态?这里宽松处理)
- if order.zhuangtai not in [1,2,3,4,5,6,7,8]:
- return Response({'code': 400, 'msg': f'订单状态 {order.zhuangtai} 不允许处罚'})
-
- # 6. 判断是否已处罚(避免重复)
- existing_punish = Chufajilu.objects.filter(
- dingdan_id=dingdan_id,
- dashouid=order.jiedan_dashou_id
- ).exists()
- if existing_punish:
- return Response({'code': 400, 'msg': '该打手已被处罚过'})
-
- # 7. 区分本地处罚还是通知对方平台
- is_cross = order.is_cross
- dispatch_type = order.dispatch_type
- partner_club_id = order.partner_club_id
- partner_order_id = order.partner_order_id
- dashou_id = order.jiedan_dashou_id
-
- # 如果没有接单打手ID,无法处罚
- if not dashou_id:
- return Response({'code': 400, 'msg': '订单无接单打手,无法处罚'})
-
- # 本地处罚(处罚我方打手)
- def local_punish():
- # 扣除打手积分(假设每次扣5分)
- dashou_user = UserMain.objects.get(yonghuid=dashou_id)
- dashou_profile = dashou_user.dashou_profile
- dashou_profile.jifen -= 5
- dashou_profile.save()
-
- # 创建处罚记录
- Chufajilu.objects.create(
- dingdan_id=dingdan_id,
- dashouid=dashou_id,
- qingqiuid='', # 请求处罚ID,可空
- chuliid=username,
- cfliyou=reason or '',
- sqzhuangtai=1, # 成功
- bhliyou='',
- ssliyou='',
- jifen=5,
- )
- logger.info(f"本地处罚打手成功: 订单 {dingdan_id}, 打手 {dashou_id}, 扣分5")
-
- # 情况A:对方派单(dispatch_type=2)或 本地订单
- if dispatch_type == 2 or not is_cross:
- # 直接本地处罚
- local_punish()
- return Response({'code': 0, 'msg': '处罚成功'})
-
- # 情况B:我方派单且有对方信息(需要通知对方平台)
- elif dispatch_type == 1 and partner_club_id and partner_order_id:
- # 需要通知对方平台处罚(对方平台打手)
- # 获取我方俱乐部ID
- my_club_config = ClubConfig.objects.filter(is_self=1).first()
- if not my_club_config:
- logger.error("未找到我方俱乐部配置")
- return Response({'code': 500, 'msg': '系统配置错误'})
- self_club_id = my_club_config.club_id
-
- # 获取对方服务器域名
- try:
- club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
- partner_domain = club_rel.partner_domain
- except ObjectDoesNotExist:
- logger.error(f"未找到合作关系: {self_club_id} <-> {partner_club_id}")
- return Response({'code': 500, 'msg': '未找到对方平台配置'})
-
- # 调用对方平台处罚接口,最多重试2次
- url = partner_domain.rstrip('/') + '/houtai/kptcfds'
- payload = {
- 'order_id': partner_order_id, # 对方平台的订单ID
- 'club_id': self_club_id,
- 'reason': reason,
- 'operator': username
- }
- timeout = 3
- retries = 2
- success = False
- for attempt in range(retries):
- try:
- resp = requests.post(url, json=payload, timeout=timeout)
- if resp.status_code == 200:
- resp_json = resp.json()
- if resp_json.get('code') == 0:
- success = True
- logger.info(f"通知对方平台处罚成功: {dingdan_id}")
- # 可选:在本平台记录处罚记录(标记已通知对方,但不扣分)
- Chufajilu.objects.create(
- dingdan_id=dingdan_id,
- dashouid=dashou_id,
- qingqiuid='',
- chuliid=username,
- cfliyou=reason or '',
- sqzhuangtai=1,
- bhliyou='',
- ssliyou='',
- jifen=0, # 对方扣分,本方不扣
- )
- break
- else:
- logger.warning(f"对方平台返回错误 (尝试{attempt+1}): {resp_json.get('msg')}")
- else:
- logger.warning(f"对方平台HTTP错误 (尝试{attempt+1}): {resp.status_code}")
- except requests.exceptions.Timeout:
- logger.warning(f"请求对方平台超时 (尝试{attempt+1}): {url}")
- except Exception as e:
- logger.warning(f"请求对方平台异常 (尝试{attempt+1}): {str(e)}")
- if not success:
- return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'})
-
- return Response({'code': 0, 'msg': '处罚成功'})
-
- else:
- # 其他情况(例如我方派单但缺少对方信息),视为本地订单,处罚我方打手
- local_punish()
- return Response({'code': 0, 'msg': '处罚成功'})
-
- except Dingdan.DoesNotExist:
- logger.warning(f"订单不存在: {dingdan_id}")
- return Response({'code': 404, 'msg': '订单不存在'})
- except Exception as e:
- logger.error(f"处罚接口异常: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-
-
-
-
-@method_decorator(csrf_exempt, name='dispatch')
-class PartnerPunishNotifyView(APIView):
- """
- 接收对方平台处罚通知接口
- """
- authentication_classes = []
- permission_classes = [AllowAny]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # token = request.data.get('token')
- # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN):
- # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200)
- try:
- order_id = request.data.get('order_id', '').strip()
- club_id = request.data.get('club_id', '').strip()
- reason = request.data.get('reason', '').strip()
- operator = request.data.get('operator', '').strip()
-
- if not order_id or not club_id:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 事务开始
- with transaction.atomic():
- # 查询订单并加锁
- try:
- order = Dingdan.objects.select_for_update().get(dingdan_id=order_id)
- except Dingdan.DoesNotExist:
- logger.warning(f"处罚通知:订单不存在 {order_id}")
- return Response({'code': 404, 'msg': '订单不存在'})
-
- dashou_id = order.jiedan_dashou_id
- if not dashou_id:
- logger.warning(f"订单 {order_id} 无接单打手,无法处罚")
- return Response({'code': 400, 'msg': '订单无接单打手'})
-
- # 检查是否已处罚
- existing = Chufajilu.objects.filter(
- dingdan_id=order_id,
- dashouid=dashou_id
- ).exists()
- if existing:
- logger.info(f"订单 {order_id} 打手 {dashou_id} 已被处罚过,跳过")
- return Response({'code': 0, 'msg': 'already punished'})
-
- # 扣除积分
- dashou_user = UserMain.objects.get(yonghuid=dashou_id)
- dashou_profile = dashou_user.dashou_profile
- dashou_profile.jifen -= 5
- dashou_profile.save()
-
- # 创建处罚记录
- Chufajilu.objects.create(
- dingdan_id=order_id,
- dashouid=dashou_id,
- qingqiuid='',
- chuliid=operator,
- cfliyou=reason or '',
- sqzhuangtai=0,
- bhliyou='',
- ssliyou='',
- jifen=5,
- )
- logger.info(f"对方平台处罚成功: 订单 {order_id}, 打手 {dashou_id}")
-
- return Response({'code': 0, 'msg': 'success'})
-
- except Exception as e:
- logger.error(f"对方处罚通知接口异常: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-
-
-
-
-
-
-# ==================== 接口1:获取俱乐部列表(权限码2) ====================
-class GetClubConfigForPreSettlementView(APIView):
- """
- 获取对方俱乐部配置列表(用于预结算页面)
- 权限要求:客服身份 + 权限代码2且开启
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- user = request.user
- # 1. 校验客服身份
- try:
- kefu = UserKefu.objects.select_related('user').get(user=user)
- except ObjectDoesNotExist:
- return Response({'code': 403, 'msg': '身份错误,非客服账号'})
- if kefu.zhuangtai != 1:
- return Response({'code': 403, 'msg': '客服账号已被禁用'})
-
- # 2. 获取请求参数中的 username 并校验
- data = request.data
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '004aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
-
- # 4. 查询所有对方俱乐部(is_self=0)
- club_configs = ClubConfig.objects.filter(is_self=0).only(
- 'club_id', 'club_nickname', 'club_avatar', 'storage_bucket_domain'
- )
- data_list = []
- for club in club_configs:
- data_list.append({
- 'club_id': club.club_id,
- 'club_nickname': club.club_nickname or club.club_id,
- 'club_avatar': club.club_avatar or '',
- 'storage_bucket_domain': club.storage_bucket_domain or ''
- })
- return Response({'code': 0, 'msg': 'success', 'data': data_list})
-
- except Exception as e:
- logger.error(f"获取俱乐部配置失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-# ==================== 接口2:获取预结算订单列表 ====================
-class GetPreSettlementDataView(APIView):
- """获取预结算订单列表(支持筛选、分页、统计)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- user = request.user
- # 1. 客服身份校验
- try:
- kefu = UserKefu.objects.select_related('user').get(user=user)
- except ObjectDoesNotExist:
- return Response({'code': 403, 'msg': '身份错误,非客服账号'})
- if kefu.zhuangtai != 1:
- return Response({'code': 403, 'msg': '客服账号已被禁用'})
-
- data = request.data
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限(003aa)
- if '004aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
-
- # 2. 获取筛选参数
- status_filter = data.get('status_filter') # 0=待处理, 1=已处理(包含已拒绝)
- club_id = data.get('club_id')
- dingdan_id = data.get('dingdan_id')
- partner_order_id = data.get('partner_order_id')
- dashou_id = data.get('dashou_id')
- clkf = data.get('clkf')
- date_str = data.get('date') # 创建日期 YYYY-MM-DD
- page = int(data.get('page', 1))
- page_size = int(data.get('pageSize', 20))
-
- # 3. 构建查询集
- queryset = PreSettlement.objects.all()
-
- # 状态筛选:0待处理,1已处理(status=1或2)
- if status_filter == 0:
- queryset = queryset.filter(status=0)
- elif status_filter == 1:
- queryset = queryset.filter(status__in=[1, 2])
- # 如果status_filter为其他值(如'all'),则不筛选
-
- if club_id:
- queryset = queryset.filter(partner_club_id=club_id)
- if dingdan_id:
- queryset = queryset.filter(dingdan_id__icontains=dingdan_id)
- if partner_order_id:
- queryset = queryset.filter(partner_order_id__icontains=partner_order_id)
- if dashou_id:
- queryset = queryset.filter(dashou_id__icontains=dashou_id)
- if clkf:
- queryset = queryset.filter(operator__icontains=clkf)
- if date_str:
- try:
- target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
- queryset = queryset.filter(create_time__date=target_date)
- except ValueError:
- pass
-
- # 4. 统计数据(待处理总数量和总金额、已处理总数量)
- pending_queryset = PreSettlement.objects.filter(status=0)
- # 应用相同的筛选条件(除了状态)
- if club_id:
- pending_queryset = pending_queryset.filter(partner_club_id=club_id)
- if dingdan_id:
- pending_queryset = pending_queryset.filter(dingdan_id__icontains=dingdan_id)
- if partner_order_id:
- pending_queryset = pending_queryset.filter(partner_order_id__icontains=partner_order_id)
- if dashou_id:
- pending_queryset = pending_queryset.filter(dashou_id__icontains=dashou_id)
- if clkf:
- pending_queryset = pending_queryset.filter(operator__icontains=clkf)
- if date_str:
- pending_queryset = pending_queryset.filter(create_time__date=target_date)
-
- pending_count = pending_queryset.count()
- pending_amount = pending_queryset.aggregate(total=Sum('order_amount'))['total'] or Decimal('0.00')
-
- processed_count = PreSettlement.objects.filter(status__in=[1, 2])
- if club_id:
- processed_count = processed_count.filter(partner_club_id=club_id)
- if dingdan_id:
- processed_count = processed_count.filter(dingdan_id__icontains=dingdan_id)
- if partner_order_id:
- processed_count = processed_count.filter(partner_order_id__icontains=partner_order_id)
- if dashou_id:
- processed_count = processed_count.filter(dashou_id__icontains=dashou_id)
- if clkf:
- processed_count = processed_count.filter(operator__icontains=clkf)
- if date_str:
- processed_count = processed_count.filter(create_time__date=target_date)
- processed_count = processed_count.count()
-
- # 5. 分页
- total = queryset.count()
- start = (page - 1) * page_size
- end = start + page_size
- items = queryset.order_by('-create_time')[start:end]
-
- # 6. 构造返回数据
- list_data = []
- for item in items:
- list_data.append({
- 'dingdan_id': item.dingdan_id,
- 'partner_order_id': item.partner_order_id,
- 'partner_club_id': item.partner_club_id,
- 'dashou_id': item.dashou_id,
- 'amount': float(item.amount),
- 'order_amount': float(item.order_amount),
- 'operator': item.operator,
- 'status': item.status,
- 'status_text': dict(PreSettlement.STATUS_CHOICES).get(item.status, '未知'),
- 'create_time': item.create_time.strftime('%Y-%m-%d %H:%M:%S') if item.create_time else '',
- 'process_time': item.process_time.strftime('%Y-%m-%d %H:%M:%S') if item.process_time else '',
- })
-
- return Response({
- 'code': 0,
- 'msg': 'success',
- 'data': {
- 'list': list_data,
- 'total': total,
- 'stats': {
- 'pending_count': pending_count,
- 'pending_amount': float(pending_amount),
- 'processed_count': processed_count
- }
- }
- })
-
- except Exception as e:
- logger.error(f"获取预结算数据失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-# ==================== 接口3:打款/拒绝操作 ====================
-class PreSettlementActionView(APIView):
- """预结算打款/拒绝操作(原子事务,保证资金安全)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- user = request.user
- # 1. 客服身份校验
- try:
- kefu = UserKefu.objects.select_related('user').get(user=user)
- except ObjectDoesNotExist:
- return Response({'code': 403, 'msg': '身份错误,非客服账号'})
- if kefu.zhuangtai != 1:
- return Response({'code': 403, 'msg': '客服账号已被禁用'})
-
- data = request.data
-
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否有跨平台管理权限()
- if '004aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权处理预结算打款'})
-
- dingdan_id = data.get('dingdan_id')
- action = data.get('action') # 'pay' 或 'reject'
-
- if not dingdan_id or action not in ['pay', 'reject']:
- return Response({'code': 400, 'msg': '参数错误'})
-
- with transaction.atomic():
- # 锁定预结算记录(行锁)
- pre_settlement = PreSettlement.objects.select_for_update().get(dingdan_id=dingdan_id)
-
- # 只有状态为0(待处理)才能操作
- if pre_settlement.status != 0:
- return Response({'code': 400, 'msg': '该记录已处理,不可重复操作'})
-
- if action == 'pay':
- # 打款操作:增加打手余额及相关统计
- dashou_id = pre_settlement.dashou_id
- amount = pre_settlement.amount
-
- # 查询打手扩展表
- try:
- dashou_user = UserMain.objects.get(yonghuid=dashou_id)
- dashou_profile = dashou_user.dashou_profile
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- return Response({'code': 404, 'msg': '打手不存在'})
-
- # 使用 F 表达式原子更新余额和统计字段(防止并发覆盖)
- dashou_profile.yue = F('yue') + amount
- dashou_profile.zonge = F('zonge') + amount
- dashou_profile.jinrishouyi = F('jinrishouyi') + amount
- dashou_profile.jinyueshouyi = F('jinyueshouyi') + amount
- dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1
- dashou_profile.save(update_fields=['yue', 'zonge', 'jinrishouyi', 'jinyueshouyi', 'chengjiaozongliang'])
-
- # 更新预结算状态为已结算
- pre_settlement.status = 1
- pre_settlement.process_time = timezone.now()
- pre_settlement.operator = user.phone
- pre_settlement.save()
-
- # ---------- 打手每日统计(成交) ----------
-
- try:
-
-
-
- from houtai.utils import update_dashou_daily_by_action
- update_dashou_daily_by_action(
- yonghuid=dashou_id,
- amount=amount,
- action=2 # 2 表示成交(结算)
- )
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
-
-
- logger.info(f"预结算打款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {amount}")
-
- logger.info(f"预结算打款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {amount}")
-
- elif action == 'reject':
- # 拒绝操作:仅修改状态,不涉及资金变动
- pre_settlement.status = 2
- pre_settlement.process_time = timezone.now()
- pre_settlement.operator = user.phone
- pre_settlement.save()
- logger.info(f"预结算拒绝: 订单 {dingdan_id}")
-
- # ---------- 打手每日统计(成交) ----------
- dashou_id = pre_settlement.dashou_id
- amount = pre_settlement.amount
- try:
- dashou_id = pre_settlement.dashou_id
- amount = pre_settlement.amount
- from houtai.utils import update_dashou_daily_by_action
- update_dashou_daily_by_action(
- yonghuid=dashou_id,
- amount=amount,
- action=3 # 3 表示退款(退款)
- )
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- logger.info(f"预结算拒绝: 订单 {dingdan_id}")
-
- return Response({'code': 0, 'msg': '操作成功'})
-
- except PreSettlement.DoesNotExist:
- return Response({'code': 404, 'msg': '预结算记录不存在'})
- except Exception as e:
- logger.error(f"预结算操作异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-
-
-
-
-class GetRolePermissionView(APIView):
- """
- 获取所有角色、权限以及角色关联的用户数
- 权限要求:用户必须拥有 000001 权限
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 1. 权限校验(验证客服身份,并获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 2. 检查是否有 000001 权限
- if '000001' not in permissions:
- return Response({'code': 403, 'msg': '您无权访问此页面'})
-
- # 3. 获取所有权限
- all_perms = Permission.objects.all().values('perm_code', 'perm_name', 'description')
- permissions_list = list(all_perms)
-
- # 4. 获取所有角色,并统计每个角色下的用户数和关联的权限
- roles = Role.objects.all()
- roles_data = []
- for role in roles:
- # 统计该角色下的用户数
- user_count = UserRole.objects.filter(role=role).count()
- # 获取该角色绑定的权限
- role_perms = RolePermission.objects.filter(role=role).select_related('permission')
- perms_of_role = [
- {
- 'perm_code': rp.permission.perm_code,
- 'perm_name': rp.permission.perm_name,
- 'description': rp.permission.description
- }
- for rp in role_perms
- ]
- roles_data.append({
- 'role_code': role.role_code,
- 'role_name': role.role_name,
- 'description': role.description,
- 'user_count': user_count,
- 'permissions': perms_of_role
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'roles': roles_data,
- 'permissions': permissions_list
- }
- })
-
-
-class ModifyRolePermissionView(APIView):
- """
- 通用接口:支持删除角色、为角色添加权限、移除角色权限
- 操作类型通过 action 字段区分
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- action = request.data.get('action') # delete_role, add_permission, remove_permission
- if not username or not action:
- return Response({'code': 400, 'msg': '缺少必要参数'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '000001' not in permissions:
- return Response({'code': 403, 'msg': '您无权进行此操作'})
-
- # 获取角色(需要角色编码)
- role_code = request.data.get('role_code')
- if not role_code:
- return Response({'code': 400, 'msg': '缺少角色编码'})
-
- try:
- role = Role.objects.get(role_code=role_code)
- except Role.DoesNotExist:
- return Response({'code': 404, 'msg': '角色不存在'})
-
- # 超级管理员角色(role_code='000001')的特殊限制
- if role.role_code == '000001':
- if action in ['delete_role']:
- return Response({'code': 403, 'msg': '超级管理员角色不能删除'})
- # 修改名称/描述(本接口不涉及名称描述修改,但若后续扩展需限制)
-
- # 根据 action 处理
- if action == 'delete_role':
- # 删除角色:同时删除角色权限关联、用户角色关联
- with transaction.atomic():
- RolePermission.objects.filter(role=role).delete()
- UserRole.objects.filter(role=role).delete()
- role.delete()
- return Response({'code': 0, 'msg': '角色删除成功'})
-
- elif action == 'add_permission':
- perm_code = request.data.get('perm_code')
- if not perm_code:
- return Response({'code': 400, 'msg': '缺少权限编码'})
- # 超级管理员角色不能添加 000001 权限(但实际它已拥有,这里防御)
- if role.role_code == '000001' and perm_code == '000001':
- return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'})
- try:
- perm = Permission.objects.get(perm_code=perm_code)
- except Permission.DoesNotExist:
- return Response({'code': 404, 'msg': '权限不存在'})
- # 检查是否已存在
- if RolePermission.objects.filter(role=role, permission=perm).exists():
- return Response({'code': 400, 'msg': '该角色已拥有此权限'})
- RolePermission.objects.create(role=role, permission=perm)
- return Response({'code': 0, 'msg': '权限添加成功'})
-
- elif action == 'remove_permission':
- perm_code = request.data.get('perm_code')
- if not perm_code:
- return Response({'code': 400, 'msg': '缺少权限编码'})
- # 超级管理员角色不能删除 000001 权限
- if role.role_code == '000001' and perm_code == '000001':
- return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'})
- try:
- perm = Permission.objects.get(perm_code=perm_code)
- except Permission.DoesNotExist:
- return Response({'code': 404, 'msg': '权限不存在'})
- deleted = RolePermission.objects.filter(role=role, permission=perm).delete()
- if deleted[0] == 0:
- return Response({'code': 400, 'msg': '该角色未拥有此权限'})
- return Response({'code': 0, 'msg': '权限移除成功'})
-
- else:
- return Response({'code': 400, 'msg': '无效的 action'})
-
-
-import random
-import string
-from django.db import transaction
-
-class AddRoleView(APIView):
- """
- 添加角色:自动生成角色编码,前端提供角色名称、描述、选择的权限列表
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- role_name = request.data.get('role_name', '').strip()
- description = request.data.get('description', '').strip()
- perm_codes = request.data.get('perm_codes', []) # 列表
-
- if not username or not role_name:
- return Response({'code': 400, 'msg': '缺少必要参数'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '000001' not in permissions:
- return Response({'code': 403, 'msg': '您无权进行此操作'})
-
- # 检查角色名称是否已存在
- if Role.objects.filter(role_name=role_name).exists():
- return Response({'code': 400, 'msg': '角色名称已存在'})
-
- # 生成唯一角色编码(格式:ROLE_ + 8位随机字母数字)
- while True:
- random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
- role_code = f"ROLE_{random_str}"
- if not Role.objects.filter(role_code=role_code).exists():
- break
-
- # 创建角色
- with transaction.atomic():
- role = Role.objects.create(
- role_code=role_code,
- role_name=role_name,
- description=description,
-
- )
- # 绑定权限(过滤掉不存在的权限编码)
- for pc in perm_codes:
- # 不允许添加 000001 权限(此权限仅供超级管理员)
- if pc == '000001':
- continue
- try:
- perm = Permission.objects.get(perm_code=pc)
- RolePermission.objects.create(role=role, permission=perm)
- except Permission.DoesNotExist:
- # 忽略不存在的权限,或记录日志
- logger.warning(f"添加角色时忽略不存在的权限编码: {pc}")
- pass
-
- return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}})
-
-
-import logging
-import traceback
-from django.core.paginator import Paginator
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from django.db.models import Exists, OuterRef # 若不再使用可移除,此处保留以备后用
-
-# 请根据项目实际路径导入 verify_kefu_permission
-from .utils import verify_kefu_permission
-
-logger = logging.getLogger(__name__)
-
-class GetAdminUserListView(APIView):
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '000001' not in permissions:
- return Response({'code': 403, 'msg': '您无权访问此页面'})
-
- phone = request.data.get('phone', '').strip()
- nicheng = request.data.get('nicheng', '').strip()
- role_code = request.data.get('roleCode', '').strip()
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 20))
-
- users = UserMain.objects.filter(
- user_type='kefu',
- kefu_profile__isnull=False
- ).select_related('kefu_profile')
-
- if phone:
- users = users.filter(phone__icontains=phone)
- if nicheng:
- users = users.filter(kefu_profile__nicheng__icontains=nicheng)
-
- # 关键修正:先求值为列表,再传入 phone__in
- if role_code:
- user_ids_with_role = list(
- UserRole.objects.filter(role__role_code=role_code)
- .values_list('account_id', flat=True)
- )
- users = users.filter(phone__in=user_ids_with_role)
-
- users = users.order_by('-create_time')
- paginator = Paginator(users, page_size)
- page_obj = paginator.get_page(page)
-
- data_list = []
- for user in page_obj:
- user_roles = UserRole.objects.filter(account_id=user.phone).select_related('role')
- roles_data = [
- {'role_code': ur.role.role_code, 'role_name': ur.role.role_name}
- for ur in user_roles
- ]
- data_list.append({
- 'phone': user.phone,
- 'nicheng': user.kefu_profile.nicheng,
- 'roles': roles_data,
- 'status': user.kefu_profile.zhuangtai,
- 'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(user, 'create_time') else '',
- 'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(user, 'update_time') else '',
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': data_list,
- 'total': paginator.count,
- 'page': page,
- 'pageSize': page_size
- }
- })
-
- except Exception as e:
- logger.error(f"GetAdminUserListView 异常: {e}\n{traceback.format_exc()}")
- return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'})
-
-
-
-class GetAdminRolesView(APIView):
- """获取所有角色(用于筛选和分配)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '000001' not in permissions:
- return Response({'code': 403, 'msg': '您无权访问此页面'})
-
- roles = Role.objects.all().values('role_code', 'role_name')
- return Response({
- 'code': 0,
- 'data': {
- 'roles': list(roles)
- }
- })
-
-
-
-
-
-
-'''class GetAdminUserListView(APIView):
- """获取后台用户列表(支持分页、筛选)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '000001' not in permissions:
- return Response({'code': 403, 'msg': '您无权访问此页面'})
-
- # 筛选条件
- phone = request.data.get('phone', '').strip()
- nicheng = request.data.get('nicheng', '').strip()
- role_code = request.data.get('roleCode', '').strip()
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 20))
-
- # 基础查询:user_type='kefu' 且存在客服扩展表
- users = UserMain.objects.filter(user_type='kefu', kefu_profile__isnull=False).select_related('kefu_profile')
- if phone:
- users = users.filter(phone__icontains=phone)
- if nicheng:
- users = users.filter(kefu_profile__nicheng__icontains=nicheng)
- if role_code:
- # 筛选拥有该角色的用户
- user_ids_with_role = UserRole.objects.filter(role__role_code=role_code).values_list('account_id', flat=True)
- users = users.filter(phone__in=user_ids_with_role)
-
- # 排序
- users = users.order_by('-create_time')
-
- # 分页
- paginator = Paginator(users, page_size)
- page_obj = paginator.get_page(page)
-
- # 构建返回数据
- data_list = []
- for user in page_obj:
- # 获取用户的所有角色
- user_roles = UserRole.objects.filter(account_id=user.phone).select_related('role')
- roles_data = [{'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles]
- data_list.append({
- 'phone': user.phone,
- 'nicheng': user.kefu_profile.nicheng,
- 'roles': roles_data,
- 'status': user.kefu_profile.zhuangtai,
- 'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S'),
- 'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S'),
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': data_list,
- 'total': paginator.count,
- 'page': page,
- 'pageSize': page_size
- }
- })'''
-
-
-
-
-
-class ModifyAdminUserView(APIView):
- """修改后台用户信息(昵称、密码、二级密码、状态、角色)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- action = request.data.get('action')
- if not username or not action:
- return Response({'code': 400, 'msg': '缺少参数'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '000001' not in permissions:
- return Response({'code': 403, 'msg': '您无权进行此操作'})
-
- target_phone = request.data.get('phone', '').strip()
- if not target_phone:
- return Response({'code': 400, 'msg': '缺少目标用户账号'})
-
- try:
- target_user = UserMain.objects.get(phone=target_phone, user_type='kefu')
- target_kefu = target_user.kefu_profile
- except UserMain.DoesNotExist:
- return Response({'code': 404, 'msg': '用户不存在'})
- except UserKefu.DoesNotExist:
- return Response({'code': 404, 'msg': '用户扩展信息缺失'})
-
- with transaction.atomic():
- if action == 'update_user_info':
- # 修改昵称、状态、密码、二级密码
- nicheng = request.data.get('nicheng')
- status = request.data.get('status')
- password = request.data.get('password')
- erjimima = request.data.get('erjimima')
- if nicheng is not None:
- target_kefu.nicheng = nicheng
- if status is not None:
- target_kefu.zhuangtai = int(status)
- if password:
- if len(password) < 6:
- return Response({'code': 400, 'msg': '密码至少6位'})
- target_user.password = password
- if erjimima:
- if len(erjimima) < 6:
- return Response({'code': 400, 'msg': '二级密码至少6位'})
- target_kefu.erjimima = erjimima # 假设二级密码也需要加密
- target_user.save()
- target_kefu.save()
- return Response({'code': 0, 'msg': '修改成功'})
-
- elif action == 'add_user_roles':
- role_codes = request.data.get('role_codes', [])
- if not role_codes:
- return Response({'code': 400, 'msg': '请选择角色'})
- for rc in role_codes:
- try:
- role = Role.objects.get(role_code=rc)
- except Role.DoesNotExist:
- continue
- UserRole.objects.get_or_create(account_id=target_phone, role=role)
- return Response({'code': 0, 'msg': '角色添加成功'})
-
- elif action == 'remove_user_role':
- role_code = request.data.get('role_code')
- if not role_code:
- return Response({'code': 400, 'msg': '缺少角色编码'})
- try:
- role = Role.objects.get(role_code=role_code)
- except Role.DoesNotExist:
- return Response({'code': 404, 'msg': '角色不存在'})
- UserRole.objects.filter(account_id=target_phone, role=role).delete()
- return Response({'code': 0, 'msg': '角色移除成功'})
-
- else:
- return Response({'code': 400, 'msg': '无效的action'})
-
-
-
-
-
-class AddAdminUserView(APIView):
- """添加后台用户(客服账号)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '000001' not in permissions:
- return Response({'code': 403, 'msg': '您无权进行此操作'})
-
- phone = request.data.get('phone', '').strip()
- password = request.data.get('password', '').strip()
- erjimima = request.data.get('erjimima', '').strip()
- nicheng = request.data.get('nicheng', '').strip()
- role_codes = request.data.get('role_codes', [])
-
- if not all([phone, password, erjimima, nicheng]):
- return Response({'code': 400, 'msg': '请填写完整信息'})
- if len(password) < 6 or len(erjimima) < 6:
- return Response({'code': 400, 'msg': '密码和二级密码至少6位'})
-
- # 校验手机号是否已被占用(且 user_type='kefu')
- if UserMain.objects.filter(phone=phone, user_type='kefu').exists():
- return Response({'code': 400, 'msg': '该账号已存在'})
-
- # 生成唯一的 yonghuid(7位数字)
- while True:
- yonghuid = str(random.randint(1000000, 9999999))
- if not UserMain.objects.filter(yonghuid=yonghuid).exists():
- break
-
- # 生成唯一的 openid(格式:admin_{时间戳}_{随机6位})
- timestamp = str(int(time.time() * 1000))
- random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
- openid = f"admin_{timestamp}_{random_suffix}"
- while UserMain.objects.filter(openid=openid).exists():
- random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
- openid = f"admin_{timestamp}_{random_suffix}"
-
- with transaction.atomic():
- user_main = UserMain.objects.create(
- yonghuid=yonghuid,
- openid=openid,
- phone=phone,
- password=password,
- user_type='kefu'
- )
- # 创建客服扩展表(后台用户扩展)
- UserKefu.objects.create(
- user=user_main,
- nicheng=nicheng,
- erjimima=erjimima,
- zhuangtai=1
- )
- # 分配角色
- for rc in role_codes:
- try:
- role = Role.objects.get(role_code=rc)
- UserRole.objects.create(account_id=phone, role=role)
- except Role.DoesNotExist:
- pass
-
- return Response({'code': 0, 'msg': '添加成功'})
-
-
-
-
-
-
-
-
-class KefuGetDashouListView(APIView):
- """
- 客服获取打手列表接口(仅打手类型)
- 需要拥有以下任一权限:001aa,001bb,001cc,001dd,001ee,001ff,001gg
- 请求:POST /yonghu/kefuhqdslb
- 参数:
- username: 客服手机号(用于权限校验)
- keyword: 搜索关键词(可选,搜索用户ID或昵称)
- page: 页码(从1开始)
- page_size: 每页数量
- 返回:
- code: 0
- data: {
- list: [打手对象],
- has_more: bool,
- total: int, // 总打手数
- valid_dashou_count: int // 有效打手数(有有效会员)
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数(前端字段为 username)
- username = request.data.get('username', '').strip()
- keyword = request.data.get('keyword', '').strip()
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 20))
- except ValueError:
- return Response({'code': 400, 'msg': '分页参数错误'})
-
- if not username:
- return Response({'code': 401, 'msg': '认证失败'})
-
- # 2. 公共权限校验(验证客服身份,获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否拥有打手管理相关权限(至少一个)
- required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'}
- if not any(p in permissions for p in required_perms):
- return Response({'code': 403, 'msg': '您没有权限查看打手列表'})
-
- # 4. 构建打手查询集(仅打手类型)
- dashou_qs = UserDashou.objects.select_related('user').all()
-
- # 关键词筛选(用户ID或昵称)
- if keyword:
- dashou_qs = dashou_qs.filter(
- Q(user__yonghuid__icontains=keyword) |
- Q(nicheng__icontains=keyword)
- )
-
- # 5. 统计打手总数
- total_dashou = dashou_qs.count()
-
- # 6. 统计有效打手数(有会员且未过期)
- dashou_user_ids = dashou_qs.values_list('user__yonghuid', flat=True)
- now = timezone.now()
- valid_user_ids = Huiyuangoumai.objects.filter(
- yonghu_id__in=dashou_user_ids,
- huiyuan_zhuangtai=1,
- daoqi_time__gt=now
- ).values_list('yonghu_id', flat=True).distinct()
- valid_dashou_count = valid_user_ids.count()
-
- # 7. 分页
- offset = (page - 1) * page_size
- dashou_list = dashou_qs.order_by('-create_time')[offset:offset + page_size]
-
- # 8. 构建返回数据
- result = []
- for dashou in dashou_list:
- user = dashou.user
- result.append({
- 'yonghuid': user.yonghuid,
- 'avatar': user.avatar or '',
- 'zaixianzhuangtai': dashou.zaixianzhuangtai,
- 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
- 'nicheng': dashou.nicheng or '',
- 'zhuangtai': dashou.zhuangtai,
- })
-
- has_more = (offset + len(dashou_list)) < total_dashou
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': result,
- 'has_more': has_more,
- 'total': total_dashou,
- 'valid_dashou_count': valid_dashou_count,
- }
- })
-
-
-
-class KefuGetDashouDetailView(APIView):
- """
- 客服获取打手详情接口
- 请求:POST /yonghu/kefuhqdsxq
- 参数:{
- "username": "客服手机号",
- "uid": "打手ID (yonghuid)"
- }
- 认证:JWT
- 返回:code=0 + 打手信息 + 会员列表 + 所有会员类型 + 客服权限列表
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- username = request.data.get('phone', '').strip()
- uid = request.data.get('uid', '').strip()
-
- if not username or not uid:
- return Response({'code': 400, 'msg': '缺少参数'})
-
- # 2. 公共权限校验(验证客服身份,获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- # 3. 检查是否拥有打手管理相关权限(至少一个)
- required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'}
- if not any(p in permissions for p in required_perms):
- return Response({'code': 403, 'msg': '您没有权限查看打手详情'})
-
- # 4. 查询打手主表和扩展表
- try:
- user_main = UserMain.objects.select_related('dashou_profile').get(
- yonghuid=uid
- )
- except UserMain.DoesNotExist:
- return Response({'code': 404, 'msg': '打手不存在'})
-
- try:
- dashou = user_main.dashou_profile
- except ObjectDoesNotExist:
- # 用户存在但未注册打手身份,返回基本信息
- user_info = {
- 'yonghuid': user_main.yonghuid,
- 'avatar': user_main.avatar or '',
- 'zaixianzhuangtai': 0,
- 'zhanghaozhuangtai': 0,
- 'zhuangtai': 0,
- 'nicheng': '',
- 'chenghao': '',
- 'dianhua': '',
- 'wechat': '',
- 'phone': user_main.phone or '',
- 'create_time': user_main.create_time,
- 'ip': user_main.ip or '',
- 'jiedanzongliang': 0,
- 'chengjiaozongliang': 0,
- 'tuikuanliang': 0,
- 'jinrijiedan': 0,
- 'yue': 0.00,
- 'zonge': 0.00,
- 'jifen': 0,
- 'yajin': 0.00,
- 'jinrishouyi': 0.00,
- 'jinyueshouyi': 0.00,
- 'yaoqingren': '',
- 'jieshao': '',
- }
- member_list = []
- # 仍然需要返回 all_members 和 permissions
- all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao'))
- return Response({
- 'code': 0,
- 'data': {
- 'user_info': user_info,
- 'member_list': member_list,
- 'all_members': all_members,
- 'permissions': permissions,
- }
- })
-
- # 5. 构建打手信息
- user_info = {
- 'yonghuid': user_main.yonghuid,
- 'avatar': user_main.avatar or '',
- 'zaixianzhuangtai': dashou.zaixianzhuangtai,
- 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
- 'zhuangtai': dashou.zhuangtai,
- 'nicheng': dashou.nicheng or '',
- 'chenghao': dashou.chenghao or '',
- 'dianhua': dashou.dianhua or '',
- 'wechat': dashou.wechat or '',
- 'phone': user_main.phone or '',
- 'create_time': user_main.create_time,
- 'ip': user_main.ip or '',
- 'jiedanzongliang': dashou.jiedanzongliang,
- 'chengjiaozongliang': dashou.chengjiaozongliang,
- 'tuikuanliang': dashou.tuikuanliang,
- 'jinrijiedan': dashou.jinrijiedan,
- 'yue': float(dashou.yue) if dashou.yue else 0.00,
- 'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
- 'jifen': dashou.jifen,
- 'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
- 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
- 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
- 'yaoqingren': dashou.yaoqingren or '',
- 'jieshao': dashou.jieshao or '',
- }
-
- # 6. 查询打手已拥有的会员(未过期)
- member_records = Huiyuangoumai.objects.filter(
- yonghu_id=uid
- ).order_by('-create_time')
- member_list = []
- for record in member_records:
- # 检查是否过期
- if hasattr(record, 'jiance_shifou_daoqi'):
- is_expired = record.jiance_shifou_daoqi()
- else:
- is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
-
- if not is_expired:
- try:
- huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id)
- huiyuan_name = huiyuan.jieshao
- except Huiyuan.DoesNotExist:
- huiyuan_name = '未知会员'
- member_list.append({
- 'id': record.id,
- 'huiyuan_name': huiyuan_name,
- 'daoqi_time': record.daoqi_time,
- 'is_active': record.huiyuan_zhuangtai == 1,
- 'huiyuan_id': record.huiyuan_id, # 用于删除操作
- })
-
- # 7. 查询所有会员类型(用于前端添加会员)
- all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao'))
-
- return Response({
- 'code': 0,
- 'data': {
- 'user_info': user_info,
- 'member_list': member_list,
- 'all_members': all_members,
- 'permissions': permissions, # 返回当前客服的权限列表
- }
- })
-
-
-
-
-'''class KefuGetDashouDetailView(APIView):
- """
- 客服获取打手详情接口
- 请求:POST /yonghu/kefuhqdsxq
- 参数:{
- "phone": "客服手机号",
- "uid": "打手ID (yonghuid)"
- }
- 认证:JWT
- 返回:code=0 + 打手信息 + 会员列表
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- username = request.data.get('phone', '').strip()
- uid = request.data.get('uid', '').strip()
-
- if not username or not uid:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
-
- # 2. 公共权限校验(验证客服身份,获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查是否拥有打手管理相关权限(至少一个)
- required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'}
- if not any(p in permissions for p in required_perms):
- return Response({'code': 403, 'msg': '您没有权限查看打手列表'})
-
- # 3. 查询打手主表和扩展表
- try:
- # 使用 select_related 预加载 dashou_profile,减少数据库查询
- user_main = UserMain.objects.select_related('dashou_profile').get(
- yonghuid=uid
- )
- except UserMain.DoesNotExist:
- return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 验证用户类型是否为打手(可选,但建议验证)
- # 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册)
- try:
- dashou = user_main.dashou_profile
- except ObjectDoesNotExist:
- # 用户存在但未注册打手身份,返回基本信息
- user_info = {
- 'yonghuid': user_main.yonghuid,
- 'avatar': user_main.avatar or '',
- 'zaixianzhuangtai': 0,
- 'zhanghaozhuangtai': 0,
- 'zhuangtai': 0,
- 'nicheng': '',
- 'chenghao': '',
- 'dianhua': '',
- 'wechat': '',
- 'phone': user_main.phone or '',
- 'create_time': user_main.create_time,
- 'ip': user_main.ip or '',
- 'jiedanzongliang': 0,
- 'chengjiaozongliang': 0,
- 'tuikuanliang': 0,
- 'jinrijiedan': 0,
- 'yue': 0.00,
- 'zonge': 0.00,
- 'jifen': 0,
- 'yajin': 0.00,
- 'jinrishouyi': 0.00,
- 'jinyueshouyi': 0.00,
- 'yaoqingren': '',
- 'jieshao': '',
- }
- member_list = []
- return Response({
- 'code': 0,
- 'data': {
- 'user_info': user_info,
- 'member_list': member_list
- }
- })
-
- # 4. 构建打手信息
- user_info = {
- 'yonghuid': user_main.yonghuid,
- 'avatar': user_main.avatar or '',
- 'zaixianzhuangtai': dashou.zaixianzhuangtai,
- 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
- 'zhuangtai': dashou.zhuangtai,
- 'nicheng': dashou.nicheng or '',
- 'chenghao': dashou.chenghao or '',
- 'dianhua': dashou.dianhua or '',
- 'wechat': dashou.wechat or '',
- 'phone': user_main.phone or '',
- 'create_time': user_main.create_time,
- 'ip': user_main.ip or '',
- 'jiedanzongliang': dashou.jiedanzongliang,
- 'chengjiaozongliang': dashou.chengjiaozongliang,
- 'tuikuanliang': dashou.tuikuanliang,
- 'jinrijiedan': dashou.jinrijiedan,
- 'yue': float(dashou.yue) if dashou.yue else 0.00,
- 'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
- 'jifen': dashou.jifen,
- 'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
- 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
- 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
- 'yaoqingren': dashou.yaoqingren or '',
- 'jieshao': dashou.jieshao or '',
- }
-
- # 5. 查询打手会员信息(仅未过期)
- member_records = Huiyuangoumai.objects.filter(
- yonghu_id=uid
- ).order_by('-create_time')
-
- member_list = []
- for record in member_records:
- # 调用模型方法检查是否过期
- if hasattr(record, 'jiance_shifou_daoqi'):
- is_expired = record.jiance_shifou_daoqi()
- else:
- # 如果方法不存在,简单判断
- is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
-
- if not is_expired:
- # 获取会员详情
- try:
- huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id)
- huiyuan_name = huiyuan.jieshao
- except Huiyuan.DoesNotExist:
- huiyuan_name = '未知会员'
-
- member_list.append({
- 'id': record.id,
- 'huiyuan_name': huiyuan_name,
- 'daoqi_time': record.daoqi_time,
- 'is_active': record.huiyuan_zhuangtai == 1,
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'user_info': user_info,
- 'member_list': member_list
- }
- })'''
-
-
-
-
-
-
-class KefuUpdateDashouView(APIView):
- """
- 客服修改打手信息接口
- 支持操作:
- - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb)
- - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb)
- - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd)
- - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee)
- - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee)
- - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee)
- - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg)
- - gai_chenghao : 修改打手称号(升级/降级金牌) (权限001hh)
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- dashou_id = request.data.get('dashou_id', '').strip()
- caozuo = request.data.get('caozuo', '').strip()
- value = request.data.get('value') # 金额、积分、天数、称号等
- huiyuan_id = request.data.get('huiyuan_id', '').strip()
-
- if not username or not dashou_id or not caozuo:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- # 3. 查询打手
- try:
- dashou_user = UserMain.objects.get(yonghuid=dashou_id)
- dashou_profile = dashou_user.dashou_profile
- except UserMain.DoesNotExist:
- return Response({'code': 404, 'msg': '打手不存在'})
- except UserDashou.DoesNotExist:
- return Response({'code': 404, 'msg': '打手扩展信息缺失'})
-
- with transaction.atomic():
- try:
- before = {
- 'yue': dashou_profile.yue,
- 'yajin': dashou_profile.yajin,
- 'jifen': dashou_profile.jifen,
- 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'zhuangtai': dashou_profile.zhuangtai,
- 'chenghao': dashou_profile.chenghao,
- }
-
- # ---------- 余额操作 ----------
- if caozuo == 'jian_yue':
- if '001aa' not in permissions:
- return Response({'code': 403, 'msg': '无权限减打手余额'})
- amount = Decimal(str(value))
- if dashou_profile.yue < amount:
- return Response({'code': 400, 'msg': '余额不足'})
- dashou_profile.yue -= amount
- dashou_profile.save()
- after = dashou_profile.yue
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- xiugaitijiao=after,
- xiugaitijiaoq=before['yue'],
- )
- return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after)}})
-
- elif caozuo == 'jia_yue':
- if '001bb' not in permissions:
- return Response({'code': 403, 'msg': '无权限加打手余额'})
- amount = Decimal(str(value))
- dashou_profile.yue += amount
- dashou_profile.save()
- after = dashou_profile.yue
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- xiugaitijiao=after,
- xiugaitijiaoq=before['yue'],
- )
- return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after)}})
-
- # ---------- 押金操作 ----------
- elif caozuo == 'jian_yajin':
- if '001aa' not in permissions:
- return Response({'code': 403, 'msg': '无权限减打手押金'})
- amount = Decimal(str(value))
- if dashou_profile.yajin < amount:
- return Response({'code': 400, 'msg': '押金不足'})
- dashou_profile.yajin -= amount
- dashou_profile.save()
- after = dashou_profile.yajin
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- yajin=after,
- yyajin=before['yajin'],
- )
- return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after)}})
-
- elif caozuo == 'jia_yajin':
- if '001bb' not in permissions:
- return Response({'code': 403, 'msg': '无权限加打手押金'})
- amount = Decimal(str(value))
- dashou_profile.yajin += amount
- dashou_profile.save()
- after = dashou_profile.yajin
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- yajin=after,
- yyajin=before['yajin'],
- )
- return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after)}})
-
- # ---------- 积分操作 ----------
- elif caozuo == 'jian_jifen':
- if '001cc' not in permissions:
- return Response({'code': 403, 'msg': '无权限减打手积分'})
- jifen = int(value)
- if dashou_profile.jifen < jifen:
- return Response({'code': 400, 'msg': '积分不足'})
- dashou_profile.jifen -= jifen
- dashou_profile.save()
- after = dashou_profile.jifen
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- jifen=after,
- yjifen=before['jifen'],
- )
- return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after}})
-
- elif caozuo == 'jia_jifen':
- if '001dd' not in permissions:
- return Response({'code': 403, 'msg': '无权限加打手积分'})
- jifen = int(value)
- dashou_profile.jifen += jifen
- dashou_profile.save()
- after = dashou_profile.jifen
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- jifen=after,
- yjifen=before['jifen'],
- )
- return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after}})
-
- # ---------- 状态修改 ----------
- elif caozuo == 'gai_zhanghaozhuangtai':
- if '001ee' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改打手账号状态'})
- new_status = int(value)
- if new_status not in [0, 1]:
- return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'})
- dashou_profile.zhanghaozhuangtai = new_status
- dashou_profile.save()
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- qitashuoming=f'修改账号状态为{new_status}',
- )
- return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}})
-
- elif caozuo == 'gai_zaixianzhuangtai':
- if '001ee' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改打手在线状态'})
- new_status = int(value)
- if new_status not in [0, 1]:
- return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'})
- dashou_profile.zaixianzhuangtai = new_status
- dashou_profile.save()
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- qitashuoming=f'修改在线状态为{new_status}',
- )
- return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}})
-
- elif caozuo == 'gai_zhuangtai':
- if '001ee' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改打手接单状态'})
- new_status = int(value)
- if new_status not in [0, 1]:
- return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'})
- dashou_profile.zhuangtai = new_status
- dashou_profile.save()
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- qitashuoming=f'修改接单状态为{new_status}',
- )
- return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}})
-
- # ---------- 会员操作 ----------
- elif caozuo == 'jia_huiyuan':
- if '001ff' not in permissions:
- return Response({'code': 403, 'msg': '无权限给打手加会员'})
- if not huiyuan_id:
- return Response({'code': 400, 'msg': '缺少会员ID'})
- days = int(value) if value else 30
- try:
- huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id)
- except Huiyuan.DoesNotExist:
- return Response({'code': 404, 'msg': '会员类型不存在'})
- record, created = Huiyuangoumai.objects.select_for_update().get_or_create(
- yonghu_id=dashou_id,
- huiyuan_id=huiyuan_id,
- defaults={
- 'huiyuan_zhuangtai': 1,
- 'jieshao': huiyuan.jieshao,
- 'daoqi_time': timezone.now() + timedelta(days=days),
- }
- )
- if not created:
- new_daoqi = record.daoqi_time + timedelta(days=days)
- record.daoqi_time = new_daoqi
- record.huiyuan_zhuangtai = 1
- record.save()
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- huiyuants=days,
- huiyuan_id=huiyuan_id,
- qitashuoming=f'加会员 {huiyuan_id},增加{days}天',
- )
- return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
-
- elif caozuo == 'jian_huiyuan':
- if '001gg' not in permissions:
- return Response({'code': 403, 'msg': '无权限给打手减会员'})
- if not huiyuan_id:
- return Response({'code': 400, 'msg': '缺少会员ID'})
- try:
- record = Huiyuangoumai.objects.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id)
- except Huiyuangoumai.DoesNotExist:
- return Response({'code': 404, 'msg': '该打手未购买此会员'})
- record.delete()
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- huiyuan_id=huiyuan_id,
- qitashuoming=f'移除会员 {huiyuan_id}',
- )
- return Response({'code': 0, 'msg': '减会员成功'})
-
- # ---------- 称号修改(金牌打手) ----------
- elif caozuo == 'gai_chenghao':
- if '001hh' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改打手称号'})
- new_chenghao = str(value).strip()
- if not new_chenghao:
- return Response({'code': 400, 'msg': '称号不能为空'})
- # 可限制合法值,这里只记录日志
- dashou_profile.chenghao = new_chenghao
- dashou_profile.save()
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- qitashuoming=f'修改称号:{before["chenghao"] or "无"} → {new_chenghao}',
- )
- return Response({'code': 0, 'msg': '修改称号成功', 'data': {'new_chenghao': new_chenghao}})
-
- else:
- return Response({'code': 400, 'msg': '未知操作类型'})
-
- except Exception as e:
- logger.error(f"修改打手信息异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-
-
-class KefuGetShangjiaListView(APIView):
- """
- 客服/管理员获取商家列表接口
- 需要拥有以下任一权限:1100a, 1100b
- 请求:POST /houtai/hqsjgllb
- 参数:
- username: 客服手机号(用于权限校验,必填)
- keyword: 搜索关键词(可选,搜索商家用户ID或昵称)
- balance_op: 余额比较符,gte(>=) / lte(<=)(可选)
- balance_value: 余额数值(可选,需与balance_op同时存在)
- order_count_op: 派单量比较符,gte(>=) / lte(<=)(可选)
- order_count_value: 派单量数值(可选,需与order_count_op同时存在)
- status: 商家状态,1=正常,2=禁用(可选)
- page: 页码,从1开始,默认1
- page_size: 每页数量,默认20
- 返回:
- code: 0 成功,其他失败
- data: {
- list: [商家对象],
- total: int, // 总商家数
- }
- """
- permission_classes = [] # 不依赖全局认证,手动校验
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- username = request.data.get('username', '').strip()
- keyword = request.data.get('keyword', '').strip()
- balance_op = request.data.get('balance_op', '').strip().lower()
- balance_value_str = request.data.get('balance_value')
- order_count_op = request.data.get('order_count_op', '').strip().lower()
- order_count_value_str = request.data.get('order_count_value')
- status_str = request.data.get('status')
-
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 20))
- if page < 1 or page_size < 1:
- raise ValueError
- except ValueError:
- return Response({'code': 400, 'msg': '分页参数错误'})
-
- if not username:
- return Response({'code': 401, 'msg': '认证失败,缺少username'})
-
- # 2. 公共权限校验(复用现有函数 verify_kefu_permission)
- # 假设 verify_kefu_permission 定义在某个公共模块,这里引入
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 检查商家管理权限(1100a 或 1100b)
- required_perms = {'1100a', '1100b'}
- if not any(p in permissions for p in required_perms):
- return Response({'code': 403, 'msg': '您没有权限查看商家列表'})
-
- # 4. 构建商家查询集(商家扩展表 + 关联用户主表)
- shangjia_qs = UserShangjia.objects.select_related('user').all()
-
- # 关键词筛选(用户ID或昵称)
- if keyword:
- shangjia_qs = shangjia_qs.filter(
- Q(user__yonghuid__icontains=keyword) |
- Q(nicheng__icontains=keyword)
- )
-
- # 余额筛选
- if balance_op and balance_value_str is not None and balance_value_str != '':
- try:
- balance_value = Decimal(str(balance_value_str))
- if balance_op == 'gte':
- shangjia_qs = shangjia_qs.filter(yue__gte=balance_value)
- elif balance_op == 'lte':
- shangjia_qs = shangjia_qs.filter(yue__lte=balance_value)
- else:
- return Response({'code': 400, 'msg': '余额比较符只支持gte或lte'})
- except Exception:
- return Response({'code': 400, 'msg': '余额数值格式错误'})
-
- # 派单总量筛选(fabu字段)
- if order_count_op and order_count_value_str is not None and order_count_value_str != '':
- try:
- order_count_value = int(order_count_value_str)
- if order_count_op == 'gte':
- shangjia_qs = shangjia_qs.filter(fabu__gte=order_count_value)
- elif order_count_op == 'lte':
- shangjia_qs = shangjia_qs.filter(fabu__lte=order_count_value)
- else:
- return Response({'code': 400, 'msg': '派单量比较符只支持gte或lte'})
- except Exception:
- return Response({'code': 400, 'msg': '派单量数值必须为整数'})
-
- # 商家状态筛选
- if status_str is not None and status_str != '':
- try:
- status_val = int(status_str)
- if status_val not in [1, 2]:
- return Response({'code': 400, 'msg': '商家状态值无效,应为1或2'})
- shangjia_qs = shangjia_qs.filter(zhuangtai=status_val)
- except ValueError:
- return Response({'code': 400, 'msg': '商家状态格式错误'})
-
- # 5. 统计总商家数(应用筛选后)
- total = shangjia_qs.count()
-
- # 6. 分页
- offset = (page - 1) * page_size
- shangjia_list = shangjia_qs.order_by('-create_time')[offset:offset + page_size]
-
- # 7. 构建返回数据
- result = []
- for shangjia in shangjia_list:
- user = shangjia.user
- result.append({
- 'yonghuid': user.yonghuid,
- 'avatar': user.avatar or '',
- 'nicheng': shangjia.nicheng or '',
- 'yue': str(shangjia.yue), # 转换为字符串,避免前端精度问题
- 'fabu': shangjia.fabu,
- 'chengjiao': shangjia.chengjiao,
- 'zhuangtai': shangjia.zhuangtai, # 1=正常,2=禁用
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': result,
- 'total': total,
- }
- })
-
-
-
-
-
-
-# yonghu/views.py 中添加
-
-class KefuGetShangjiaDetailView(APIView):
- """
- 客服/管理员获取商家详情接口
- 需要权限:1100a 或 1100b
- 请求:POST /houtai/hthqsjxq
- 参数:
- username: 客服手机号(用于权限校验,必填)
- yonghuid: 商家用户ID(必填)
- 返回:
- code: 0 成功
- data: 商家所有字段(含主表头像等)
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- yonghuid = request.data.get('yonghuid', '').strip()
-
- if not username:
- return Response({'code': 401, 'msg': '认证失败,缺少username'})
- if not yonghuid:
- return Response({'code': 400, 'msg': '缺少商家ID'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- required_perms = {'1100a', '1100b'}
- if not any(p in permissions for p in required_perms):
- return Response({'code': 403, 'msg': '您没有权限查看商家详情'})
-
- # 查询商家
- try:
- shangjia = UserShangjia.objects.select_related('user').get(user__yonghuid=yonghuid)
- except UserShangjia.DoesNotExist:
- return Response({'code': 404, 'msg': '商家不存在'})
-
- user = shangjia.user
-
- data = {
- 'yonghuid': user.yonghuid,
- 'avatar': user.avatar or '',
- 'nicheng': shangjia.nicheng,
- 'zhuangtai': shangjia.zhuangtai, # 1正常,2禁用
- 'dianhua': shangjia.dianhua,
- 'wechat': shangjia.wechat,
- 'yue': str(shangjia.yue),
- 'fabu': shangjia.fabu,
- 'chengjiao': shangjia.chengjiao,
- 'tuikuan': shangjia.tuikuan,
- 'jinridingdan': shangjia.jinridingdan,
- 'jinriliushui': str(shangjia.jinriliushui),
- 'jinyuedingdan': shangjia.jinyuedingdan,
- 'jinyueliushui': str(shangjia.jinyueliushui),
- 'create_time': shangjia.create_time.isoformat() if shangjia.create_time else None,
- }
-
- return Response({'code': 0, 'data': data})
-
-
-
-
-
-
-
-class KefuUpdateShangjiaView(APIView):
- """
- 客服/管理员修改商家信息(余额、状态)
- POST /houtai/xgsjxx
- 参数:
- username, yonghuid, action
- action可选:add_balance, sub_balance, ban, unban
- amount(add_balance/sub_balance时必填)
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- yonghuid = request.data.get('yonghuid', '').strip()
- action = request.data.get('action', '').strip().lower()
-
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
- if not yonghuid:
- return Response({'code': 400, 'msg': '缺少商家ID'})
- if action not in ('add_balance', 'sub_balance', 'ban', 'unban'):
- return Response({'code': 400, 'msg': 'action参数无效'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- # 操作权限校验
- if action in ('add_balance', 'ban', 'unban') and '1100a' not in permissions:
- return Response({'code': 403, 'msg': '无权限执行此操作(需要1100a)'})
- if action == 'sub_balance' and '1100b' not in permissions:
- return Response({'code': 403, 'msg': '无权限减少商家余额(需要1100b)'})
-
- # 使用事务,select_for_update 必须在事务内
- with transaction.atomic():
- try:
- shangjia = UserShangjia.objects.select_for_update().get(user__yonghuid=yonghuid)
- except UserShangjia.DoesNotExist:
- return Response({'code': 404, 'msg': '商家不存在'})
-
- # 增加余额
- if action == 'add_balance':
- amount_str = request.data.get('amount')
- try:
- amount = Decimal(str(amount_str))
- if amount <= 0 or amount > 1000000:
- return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'})
- except:
- return Response({'code': 400, 'msg': '金额格式错误'})
- new_yue = shangjia.yue + amount
- if new_yue > 999999999.99:
- return Response({'code': 400, 'msg': '增加后余额超出上限'})
- shangjia.yue = new_yue
- shangjia.save(update_fields=['yue'])
- return Response({'code': 0, 'msg': '余额增加成功', 'data': {'new_balance': str(shangjia.yue)}})
-
- # 减少余额
- if action == 'sub_balance':
- amount_str = request.data.get('amount')
- try:
- amount = Decimal(str(amount_str))
- if amount <= 0 or amount > 1000000:
- return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'})
- except:
- return Response({'code': 400, 'msg': '金额格式错误'})
- if shangjia.yue < amount:
- return Response({'code': 400, 'msg': '余额不足'})
- shangjia.yue = shangjia.yue - amount
- shangjia.save(update_fields=['yue'])
- return Response({'code': 0, 'msg': '余额减少成功', 'data': {'new_balance': str(shangjia.yue)}})
-
- # 封禁
- if action == 'ban':
- if shangjia.zhuangtai == 2:
- return Response({'code': 400, 'msg': '商家已是封禁状态'})
- shangjia.zhuangtai = 2
- shangjia.save(update_fields=['zhuangtai'])
- return Response({'code': 0, 'msg': '商家已封禁', 'data': {'zhuangtai': 2}})
-
- # 解封
- if action == 'unban':
- if shangjia.zhuangtai == 1:
- return Response({'code': 400, 'msg': '商家已是正常状态'})
- shangjia.zhuangtai = 1
- shangjia.save(update_fields=['zhuangtai'])
- return Response({'code': 0, 'msg': '商家已解封', 'data': {'zhuangtai': 1}})
-
-
-
-
-
-
-
-class GetProductBaseDataView(APIView):
- """
- 获取商品基础数据(类型、专区、会员)
- 权限:2200a
- POST /houtai/htsphqjcsx
- 参数:username
- 返回:
- code:0, data: {
- type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...],
- zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...],
- member_list: [ {huiyuan_id, jieshao, jiage}, ...]
- }
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '2200a' not in permissions:
- return Response({'code': 403, 'msg': '无权限管理商品'})
-
- # 获取所有商品类型(按 paixu 升序)
- type_qs = ShangpinLeixing.objects.all().order_by('paixu')
- type_list = []
- for t in type_qs:
- # 🔥 修改:统计该类型下的公共商品数量(店铺为空且店铺商品类型为空)
- product_count = Shangpin.objects.filter(
- leixing_id=t.id,
- dianpu__isnull=True, # 所属店铺为空
- dianpu_leixing__isnull=True # 店铺商品类型为空
- ).count()
- type_list.append({
- 'id': t.id,
- 'jieshao': t.jieshao or '',
- 'tupian_url': t.tupian_url or '',
- 'paixu': t.paixu,
- 'shenhezhuangtai': t.shenhezhuangtai,
- 'product_count': product_count,
- })
-
- # 获取所有专区(按 paixu 升序)
- zone_qs = ShangpinZhuanqu.objects.all().order_by('paixu')
- zone_list = []
- for z in zone_qs:
- # 🔥 修改:统计该专区下的公共商品数量(店铺为空且店铺商品类型为空)
- product_count = Shangpin.objects.filter(
- zhuanqu_id=z.id,
- dianpu__isnull=True, # 所属店铺为空
- dianpu_leixing__isnull=True # 店铺商品类型为空
- ).count()
- zone_list.append({
- 'id': z.id,
- 'mingzi': z.mingzi or '',
- 'leixing_id': z.leixing_id,
- 'paixu': z.paixu,
- 'shenhezhuangtai': z.shenhezhuangtai,
- 'product_count': product_count,
- })
-
- # 获取所有会员(只需 ID、介绍、价格)
- member_list = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage'))
-
- return Response({
- 'code': 0,
- 'data': {
- 'type_list': type_list,
- 'zone_list': zone_list,
- 'member_list': member_list,
- }
- })
-
-
-
-class GetProductListView(APIView):
- """
- 获取商品列表(支持筛选、排序)
- 权限:2200a
- POST /houtai/hthqsplb
- 参数:
- username: 必填
- leixing_id: 类型ID(可选)
- zhuanqu_id: 专区ID(可选)
- keyword: 搜索关键词(商品标题/介绍)
- sales_op: 销量比较符 gte/lte
- sales_value: 销量数值
- stock_op: 库存比较符 gte/lte
- stock_value: 库存数值
- sort_field: 排序字段 zhenshi_xiaoliang / jiage
- sort_order: asc/desc
- page: 页码(默认1)
- page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留)
- 返回:
- code:0, data: { list: [...], total }
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '2200a' not in permissions:
- return Response({'code': 403, 'msg': '无权限管理商品'})
-
- # 获取参数
- leixing_id = request.data.get('leixing_id')
- zhuanqu_id = request.data.get('zhuanqu_id')
- keyword = request.data.get('keyword', '').strip()
- sales_op = request.data.get('sales_op', '').strip().lower()
- sales_value = request.data.get('sales_value')
- stock_op = request.data.get('stock_op', '').strip().lower()
- stock_value = request.data.get('stock_value')
- sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage
- sort_order = request.data.get('sort_order', 'desc').lower()
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 1000))
- if page < 1 or page_size < 1:
- raise ValueError
- except:
- return Response({'code': 400, 'msg': '分页参数错误'})
-
- # 🔥 修改:构建查询集,增加公共商品筛选(店铺为空且店铺商品类型为空)
- qs = Shangpin.objects.filter(
- dianpu__isnull=True, # 所属店铺为空
- dianpu_leixing__isnull=True # 店铺商品类型为空
- )
-
- if leixing_id:
- try:
- leixing_id = int(leixing_id)
- qs = qs.filter(leixing_id=leixing_id)
- except:
- pass
- if zhuanqu_id:
- try:
- zhuanqu_id = int(zhuanqu_id)
- qs = qs.filter(zhuanqu_id=zhuanqu_id)
- except:
- pass
- if keyword:
- qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword))
-
- # 销量筛选
- if sales_op and sales_value is not None:
- try:
- sales_val = int(sales_value)
- if sales_op == 'gte':
- qs = qs.filter(zhenshi_xiaoliang__gte=sales_val)
- elif sales_op == 'lte':
- qs = qs.filter(zhenshi_xiaoliang__lte=sales_val)
- except:
- pass
-
- # 库存筛选
- if stock_op and stock_value is not None:
- try:
- stock_val = int(stock_value)
- if stock_op == 'gte':
- qs = qs.filter(kucun__gte=stock_val)
- elif stock_op == 'lte':
- qs = qs.filter(kucun__lte=stock_val)
- except:
- pass
-
- # 排序
- if sort_field in ('zhenshi_xiaoliang', 'jiage'):
- if sort_order == 'asc':
- qs = qs.order_by(sort_field)
- else:
- qs = qs.order_by(f'-{sort_field}')
- else:
- qs = qs.order_by('-create_time') # 默认按创建时间倒序
-
- # 分页
- total = qs.count()
- offset = (page - 1) * page_size
- products = qs[offset:offset + page_size]
-
- # 构建返回数据
- product_list = []
- for p in products:
- product_list.append({
- 'id': p.id,
- 'biaoqian': p.biaoqian or '',
- 'jiage': str(p.jiage),
- 'kucun': p.kucun,
- 'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
- 'tupian_url': p.tupian_url or '',
- 'jieshao': p.jieshao or '',
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': product_list,
- 'total': total,
- }
- })
-
-
-
-
-'''class GetProductBaseDataView(APIView):
- """
- 获取商品基础数据(类型、专区、会员)
- 权限:2200a
- POST /houtai/htsphqjcsx
- 参数:username
- 返回:
- code:0, data: {
- type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...],
- zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...],
- member_list: [ {huiyuan_id, jieshao, jiage}, ...]
- }
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '2200a' not in permissions:
- return Response({'code': 403, 'msg': '无权限管理商品'})
-
- # 获取所有商品类型(按 paixu 升序)
- type_qs = ShangpinLeixing.objects.all().order_by('paixu')
- type_list = []
- for t in type_qs:
- # 统计该类型下的商品数量(忽略审核状态?通常统计所有商品,可根据需求调整)
- product_count = Shangpin.objects.filter(leixing_id=t.id).count()
- type_list.append({
- 'id': t.id,
- 'jieshao': t.jieshao or '',
- 'tupian_url': t.tupian_url or '',
- 'paixu': t.paixu,
- 'shenhezhuangtai': t.shenhezhuangtai,
- 'product_count': product_count,
- })
-
- # 获取所有专区(按 paixu 升序)
- zone_qs = ShangpinZhuanqu.objects.all().order_by('paixu')
- zone_list = []
- for z in zone_qs:
- product_count = Shangpin.objects.filter(zhuanqu_id=z.id).count()
- zone_list.append({
- 'id': z.id,
- 'mingzi': z.mingzi or '',
- 'leixing_id': z.leixing_id,
- 'paixu': z.paixu,
- 'shenhezhuangtai': z.shenhezhuangtai,
- 'product_count': product_count,
- })
-
- # 获取所有会员(只需 ID、介绍、价格)
- member_list = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage'))
-
- return Response({
- 'code': 0,
- 'data': {
- 'type_list': type_list,
- 'zone_list': zone_list,
- 'member_list': member_list,
- }
- })
-
-
-class GetProductListView(APIView):
- """
- 获取商品列表(支持筛选、排序)
- 权限:2200a
- POST /houtai/hthqsplb
- 参数:
- username: 必填
- leixing_id: 类型ID(可选)
- zhuanqu_id: 专区ID(可选)
- keyword: 搜索关键词(商品标题/介绍)
- sales_op: 销量比较符 gte/lte
- sales_value: 销量数值
- stock_op: 库存比较符 gte/lte
- stock_value: 库存数值
- sort_field: 排序字段 zhenshi_xiaoliang / jiage
- sort_order: asc/desc
- page: 页码(默认1)
- page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留)
- 返回:
- code:0, data: { list: [...], total }
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '2200a' not in permissions:
- return Response({'code': 403, 'msg': '无权限管理商品'})
-
- # 获取参数
- leixing_id = request.data.get('leixing_id')
- zhuanqu_id = request.data.get('zhuanqu_id')
- keyword = request.data.get('keyword', '').strip()
- sales_op = request.data.get('sales_op', '').strip().lower()
- sales_value = request.data.get('sales_value')
- stock_op = request.data.get('stock_op', '').strip().lower()
- stock_value = request.data.get('stock_value')
- sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage
- sort_order = request.data.get('sort_order', 'desc').lower()
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 1000))
- if page < 1 or page_size < 1:
- raise ValueError
- except:
- return Response({'code': 400, 'msg': '分页参数错误'})
-
- # 构建查询集
- qs = Shangpin.objects.all()
-
- if leixing_id:
- try:
- leixing_id = int(leixing_id)
- qs = qs.filter(leixing_id=leixing_id)
- except:
- pass
- if zhuanqu_id:
- try:
- zhuanqu_id = int(zhuanqu_id)
- qs = qs.filter(zhuanqu_id=zhuanqu_id)
- except:
- pass
- if keyword:
- qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword))
-
- # 销量筛选
- if sales_op and sales_value is not None:
- try:
- sales_val = int(sales_value)
- if sales_op == 'gte':
- qs = qs.filter(zhenshi_xiaoliang__gte=sales_val)
- elif sales_op == 'lte':
- qs = qs.filter(zhenshi_xiaoliang__lte=sales_val)
- except:
- pass
-
- # 库存筛选
- if stock_op and stock_value is not None:
- try:
- stock_val = int(stock_value)
- if stock_op == 'gte':
- qs = qs.filter(kucun__gte=stock_val)
- elif stock_op == 'lte':
- qs = qs.filter(kucun__lte=stock_val)
- except:
- pass
-
- # 排序
- if sort_field in ('zhenshi_xiaoliang', 'jiage'):
- if sort_order == 'asc':
- qs = qs.order_by(sort_field)
- else:
- qs = qs.order_by(f'-{sort_field}')
- else:
- qs = qs.order_by('-create_time') # 默认按创建时间倒序
-
- # 分页
- total = qs.count()
- offset = (page - 1) * page_size
- products = qs[offset:offset + page_size]
-
- # 构建返回数据
- product_list = []
- for p in products:
- product_list.append({
- 'id': p.id,
- 'biaoqian': p.biaoqian or '',
- 'jiage': str(p.jiage),
- 'kucun': p.kucun,
- 'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
- 'tupian_url': p.tupian_url or '',
- 'jieshao': p.jieshao or '',
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': product_list,
- 'total': total,
- }
- })'''
-
-
-class SaveProductOrderView(APIView):
- """
- 保存商品类型或专区的排序
- 权限:2200a
- POST /houtai/htsppaixu
- 参数:
- username: 必填
- type: 'leixing' 或 'zhuanqu'
- leixing_id: 当 type='zhuanqu' 时必填(用于限定该类型下的专区)
- items: [ {id, paixu}, ... ] # 每个项目的新排序值(从1开始)
- 返回:
- code:0 成功
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '2200a' not in permissions:
- return Response({'code': 403, 'msg': '无权限管理商品'})
-
- sort_type = request.data.get('type', '').strip().lower()
- if sort_type not in ('leixing', 'zhuanqu'):
- return Response({'code': 400, 'msg': 'type参数必须为leixing或zhuanqu'})
-
- items = request.data.get('items')
- if not isinstance(items, list) or len(items) == 0:
- return Response({'code': 400, 'msg': 'items参数无效'})
-
- # 校验每个 item 的格式
- for item in items:
- if not isinstance(item, dict) or 'id' not in item or 'paixu' not in item:
- return Response({'code': 400, 'msg': 'items内每项需包含id和paixu'})
- try:
- item['id'] = int(item['id'])
- item['paixu'] = int(item['paixu'])
- if item['paixu'] < 1:
- return Response({'code': 400, 'msg': 'paixu必须为正整数'})
- except:
- return Response({'code': 400, 'msg': 'id或paixu格式错误'})
-
- with transaction.atomic():
- if sort_type == 'leixing':
- # 更新商品类型的排序
- for item in items:
- ShangpinLeixing.objects.filter(id=item['id']).update(paixu=item['paixu'])
- else: # zhuanqu
- leixing_id = request.data.get('leixing_id')
- if not leixing_id:
- return Response({'code': 400, 'msg': '更新专区排序时必须提供leixing_id'})
- try:
- leixing_id = int(leixing_id)
- except:
- return Response({'code': 400, 'msg': 'leixing_id格式错误'})
- # 可选:检查该专区是否属于该类型,但不强制,由前端保证
- for item in items:
- ShangpinZhuanqu.objects.filter(id=item['id']).update(paixu=item['paixu'])
-
- return Response({'code': 0, 'msg': '排序保存成功'})
-
-
-class AddProductView(APIView):
- """
- 添加商品(含图片上传)
- 权限:2200a
- POST /houtai/htfbsp
- 参数:multipart/form-data,包含所有商品字段及图片文件
- 字段说明见代码
- """
- permission_classes = []
- parser_classes = [MultiPartParser, FormParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '2200a' not in permissions:
- return Response({'code': 403, 'msg': '无权限管理商品'})
-
- # 获取表单字段
- biaoqian = request.data.get('biaoqian', '').strip()
- jiage_str = request.data.get('jiage')
- kucun_str = request.data.get('kucun')
- duiwai_xiaoliang_str = request.data.get('duiwai_xiaoliang', '0')
- leixing_id_str = request.data.get('leixing_id')
- zhuanqu_id_str = request.data.get('zhuanqu_id')
- yaoqiuleixing_str = request.data.get('yaoqiuleixing', '1')
- huiyuan_id = request.data.get('huiyuan_id', '').strip()
- yongjin_str = request.data.get('yongjin', '0')
- fencheng_type = request.data.get('fencheng_type', 'default')
- ewai_dashou_fencheng_str = request.data.get('ewai_dashou_fencheng', '0')
- jieshao = request.data.get('jieshao', '').strip()
- xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip()
- # 图片文件
- image_file = request.FILES.get('file') # 前端上传时字段名为 'file'
-
- # 必填字段校验
- if not biaoqian:
- return Response({'code': 400, 'msg': '商品标题不能为空'})
- try:
- jiage = Decimal(jiage_str)
- if jiage <= 0:
- return Response({'code': 400, 'msg': '价格必须大于0'})
- except:
- return Response({'code': 400, 'msg': '价格格式错误'})
- try:
- kucun = int(kucun_str)
- if kucun < 0:
- return Response({'code': 400, 'msg': '库存不能为负数'})
- except:
- return Response({'code': 400, 'msg': '库存格式错误'})
- try:
- duiwai_xiaoliang = int(duiwai_xiaoliang_str)
- except:
- duiwai_xiaoliang = 0
- try:
- leixing_id = int(leixing_id_str)
- except:
- return Response({'code': 400, 'msg': '商品类型ID无效'})
- try:
- zhuanqu_id = int(zhuanqu_id_str)
- except:
- return Response({'code': 400, 'msg': '商品专区ID无效'})
- try:
- yaoqiuleixing = int(yaoqiuleixing_str)
- if yaoqiuleixing not in (1, 2):
- raise ValueError
- except:
- return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'})
-
- # 校验类型和专区的存在性及关联性
- try:
- leixing_obj = ShangpinLeixing.objects.get(id=leixing_id)
- except ShangpinLeixing.DoesNotExist:
- return Response({'code': 400, 'msg': '商品类型不存在'})
- try:
- zhuanqu_obj = ShangpinZhuanqu.objects.get(id=zhuanqu_id)
- if zhuanqu_obj.leixing_id != leixing_id:
- return Response({'code': 400, 'msg': '专区不属于该商品类型'})
- except ShangpinZhuanqu.DoesNotExist:
- return Response({'code': 400, 'msg': '商品专区不存在'})
-
- # 处理抢单要求相关字段
- huiyuan_id_val = None
- yongjin_val = None
- if yaoqiuleixing == 1:
- # 会员要求
- if not huiyuan_id:
- # 未传递时,尝试从商品类型中获取默认会员ID
- huiyuan_id_val = leixing_obj.huiyuan_id
- if not huiyuan_id_val:
- return Response({'code': 400, 'msg': '未指定会员且商品类型无默认会员'})
- else:
- huiyuan_id_val = huiyuan_id
- # 验证会员是否存在
- if not Huiyuan.objects.filter(huiyuan_id=huiyuan_id_val).exists():
- return Response({'code': 400, 'msg': '指定的会员不存在'})
- else:
- # 佣金要求
- try:
- yongjin_val = Decimal(yongjin_str)
- if yongjin_val < 0:
- return Response({'code': 400, 'msg': '佣金不能为负数'})
- except:
- # 若未传递或无效,尝试从商品类型中获取默认佣金
- if leixing_obj.yongjin is not None:
- yongjin_val = leixing_obj.yongjin
- else:
- return Response({'code': 400, 'msg': '佣金格式错误且类型无默认佣金'})
-
- # 处理打手分成方式
- kaioi_ewai = False
- ewai_fencheng = Decimal('0')
- if fencheng_type == 'custom':
- try:
- ewai_fencheng = Decimal(ewai_dashou_fencheng_str)
- if ewai_fencheng < 0:
- return Response({'code': 400, 'msg': '自定义分成金额不能为负数'})
- kaioi_ewai = True
- except:
- return Response({'code': 400, 'msg': '自定义分成金额格式错误'})
-
- # 图片上传处理
- tupian_url = ''
- if image_file:
- # 验证图片
- valid, msg = validate_image(image_file)
- if not valid:
- return Response({'code': 400, 'msg': f'图片无效:{msg}'})
- # 生成唯一文件名
- ext = image_file.name.split('.')[-1].lower()
- if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']:
- return Response({'code': 400, 'msg': '不支持的图片格式'})
- timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
- random_str = uuid.uuid4().hex[:8]
- filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}"
- # 上传到 OSS
- uploaded_url = upload_to_oss(image_file, filename)
- if not uploaded_url:
- return Response({'code': 500, 'msg': '图片上传失败,请稍后重试'})
- tupian_url = filename # 存储相对路径,前端拼接域名
- else:
- return Response({'code': 400, 'msg': '商品图片不能为空'})
-
- # 创建商品
- try:
- with transaction.atomic():
- product = Shangpin.objects.create(
- biaoqian=biaoqian,
- jiage=jiage,
- kucun=kucun,
- leixing_id=leixing_id,
- zhuanqu_id=zhuanqu_id,
- zhenshi_xiaoliang=0, # 初始真实销量为0
- duiwai_xiaoliang=duiwai_xiaoliang,
- jieshao=jieshao,
- xiadan_xuzhi=xiadan_xuzhi,
- tupian_url=tupian_url,
- yaoqiuleixing=yaoqiuleixing,
- huiyuan_id=huiyuan_id_val,
- yongjin=yongjin_val,
- kaioi_ewai_dashou_fencheng=kaioi_ewai,
- ewai_dashou_fencheng=ewai_fencheng,
- paixu=0, # 新商品默认排序最后
- shenhezhuangtai=1, # 正常状态
- )
- except Exception as e:
- logger.error(f"创建商品失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '创建商品失败,请稍后重试'})
-
- return Response({
- 'code': 0,
- 'msg': '商品发布成功',
- 'data': {
- 'id': product.id,
- 'tupian_url': tupian_url,
- }
- })
-
-
-# houtai/views_product.py 中添加
-
-class DeleteProductView(APIView):
- """
- 删除商品(物理删除,同时删除OSS图片)
- 权限:2200a
- POST /houtai/htscsp
- 参数:
- username: 客服/管理员账号
- id: 商品ID
- 返回:
- code:0 成功
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- product_id = request.data.get('id')
-
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
- if not product_id:
- return Response({'code': 400, 'msg': '缺少商品ID'})
- try:
- product_id = int(product_id)
- except:
- return Response({'code': 400, 'msg': '商品ID格式错误'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '2200a' not in permissions:
- return Response({'code': 403, 'msg': '无权限管理商品'})
-
- # 查询商品是否存在
- try:
- product = Shangpin.objects.get(id=product_id)
- except Shangpin.DoesNotExist:
- return Response({'code': 404, 'msg': '商品不存在'})
-
- # 先删除OSS上的图片
- tupian_url = product.tupian_url
- if tupian_url:
- # 调用已有的 delete_from_oss 函数
- from utils.oss_utils import delete_from_oss
- delete_success = delete_from_oss(tupian_url)
- if not delete_success:
- logger.warning(f"删除商品图片失败: {tupian_url}, 商品ID: {product_id}")
- # 注意:即使图片删除失败,也应继续删除数据库记录,避免数据不一致
- # 但可以记录日志供后续人工处理
-
- # 删除数据库记录
- product.delete()
-
- logger.info(f"商品删除成功: id={product_id}, 图片路径={tupian_url}, 操作人={username}")
- return Response({'code': 0, 'msg': '商品删除成功'})
-
-
-
-
-
-
-
-class GetProductDetailView(APIView):
- """
- 获取商品详情(包含商品类型、专区、会员列表)
- 权限:2200a
- POST /houtai/htsphqxq
- 参数:
- username: 客服/管理员账号
- id: 商品ID
- 返回:
- code:0, data: { product, type_list, zone_list, member_list }
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- product_id = request.data.get('id')
-
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
- if not product_id:
- return Response({'code': 400, 'msg': '缺少商品ID'})
- try:
- product_id = int(product_id)
- except ValueError:
- return Response({'code': 400, 'msg': '商品ID格式错误'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '2200a' not in permissions:
- return Response({'code': 403, 'msg': '无权限管理商品'})
-
- # 查询商品
- try:
- product = Shangpin.objects.get(id=product_id)
- except Shangpin.DoesNotExist:
- return Response({'code': 404, 'msg': '商品不存在'})
-
- # 查询所有商品类型、专区、会员(用于前端下拉选择)
- type_list = list(ShangpinLeixing.objects.all().values(
- 'id', 'jieshao', 'tupian_url', 'shenhezhuangtai'
- ))
- zone_list = list(ShangpinZhuanqu.objects.all().values(
- 'id', 'mingzi', 'leixing_id', 'shenhezhuangtai'
- ))
- member_list = list(Huiyuan.objects.all().values(
- 'huiyuan_id', 'jieshao', 'jiage'
- ))
-
- # 构建商品返回数据
- product_data = {
- 'id': product.id,
- 'biaoqian': product.biaoqian,
- 'jiage': str(product.jiage),
- 'kucun': product.kucun,
- 'leixing_id': product.leixing_id,
- 'zhuanqu_id': product.zhuanqu_id,
- 'zhenshi_xiaoliang': product.zhenshi_xiaoliang,
- 'duiwai_xiaoliang': product.duiwai_xiaoliang,
- 'jieshao': product.jieshao,
- 'xiadan_xuzhi': product.xiadan_xuzhi,
- 'guize_tupian': product.guize_tupian,
- 'yaoqiuleixing': product.yaoqiuleixing,
- 'huiyuan_id': product.huiyuan_id,
- 'yongjin': str(product.yongjin) if product.yongjin is not None else None,
- 'tupian_url': product.tupian_url,
- 'kaioi_ewai_dashou_fencheng': product.kaioi_ewai_dashou_fencheng,
- 'ewai_dashou_fencheng': str(product.ewai_dashou_fencheng) if product.ewai_dashou_fencheng is not None else '0',
- 'create_time': product.create_time.isoformat() if product.create_time else None,
- 'shenhezhuangtai': product.shenhezhuangtai,
- }
-
- return Response({
- 'code': 0,
- 'data': {
- 'product': product_data,
- 'type_list': type_list,
- 'zone_list': zone_list,
- 'member_list': member_list,
- }
- })
-
-
-
-class UpdateProductView(APIView):
- """
- 修改商品(支持商品图片和规则图片的更新)
- 权限:2200a
- POST /houtai/bcspxg
- """
- permission_classes = []
- parser_classes = [MultiPartParser, FormParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- product_id = request.data.get('id')
-
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
- if not product_id:
- return Response({'code': 400, 'msg': '缺少商品ID'})
- try:
- product_id = int(product_id)
- except ValueError:
- return Response({'code': 400, 'msg': '商品ID格式错误'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '2200a' not in permissions:
- return Response({'code': 403, 'msg': '无权限管理商品'})
-
- # 使用事务,将 select_for_update 放在事务内
- with transaction.atomic():
- try:
- product = Shangpin.objects.select_for_update().get(id=product_id)
- except Shangpin.DoesNotExist:
- return Response({'code': 404, 'msg': '商品不存在'})
-
- # ---------- 1. 基础字段 ----------
- biaoqian = request.data.get('biaoqian', '').strip()
- if not biaoqian:
- return Response({'code': 400, 'msg': '商品标题不能为空'})
-
- try:
- jiage = Decimal(str(request.data.get('jiage', 0)))
- if jiage <= 0:
- return Response({'code': 400, 'msg': '价格必须大于0'})
- except:
- return Response({'code': 400, 'msg': '价格格式错误'})
-
- try:
- kucun = int(request.data.get('kucun', 0))
- if kucun < 0:
- return Response({'code': 400, 'msg': '库存不能为负数'})
- except:
- return Response({'code': 400, 'msg': '库存格式错误'})
-
- try:
- duiwai_xiaoliang = int(request.data.get('duiwai_xiaoliang', 0))
- except:
- duiwai_xiaoliang = 0
-
- try:
- leixing_id = int(request.data.get('leixing_id'))
- except:
- return Response({'code': 400, 'msg': '商品类型ID无效'})
-
- try:
- zhuanqu_id = int(request.data.get('zhuanqu_id'))
- except:
- return Response({'code': 400, 'msg': '商品专区ID无效'})
-
- try:
- yaoqiuleixing = int(request.data.get('yaoqiuleixing', 1))
- if yaoqiuleixing not in (1, 2):
- raise ValueError
- except:
- return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'})
-
- try:
- shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1))
- except:
- shenhezhuangtai = 1
-
- # 校验类型和专区存在性
- if not ShangpinLeixing.objects.filter(id=leixing_id).exists():
- return Response({'code': 400, 'msg': '商品类型不存在'})
- if not ShangpinZhuanqu.objects.filter(id=zhuanqu_id, leixing_id=leixing_id).exists():
- return Response({'code': 400, 'msg': '专区不存在或不属于该类型'})
-
- # ---------- 2. 会员/佣金 ----------
- huiyuan_id = None
- yongjin = None
- if yaoqiuleixing == 1:
- huiyuan_id = request.data.get('huiyuan_id', '').strip() or None
- if huiyuan_id and not Huiyuan.objects.filter(huiyuan_id=huiyuan_id).exists():
- return Response({'code': 400, 'msg': '指定的会员不存在'})
- else:
- try:
- yongjin = Decimal(str(request.data.get('yongjin', 0)))
- if yongjin < 0:
- return Response({'code': 400, 'msg': '佣金不能为负数'})
- except:
- yongjin = Decimal('0')
-
- # ---------- 3. 打手分成 ----------
- kaioi_ewai = request.data.get('kaioi_ewai_dashou_fencheng') == 'true'
- ewai_fencheng = Decimal('0')
- if kaioi_ewai:
- try:
- ewai_fencheng = Decimal(str(request.data.get('ewai_dashou_fencheng', 0)))
- if ewai_fencheng < 0:
- return Response({'code': 400, 'msg': '分成金额不能为负数'})
- except:
- return Response({'code': 400, 'msg': '分成金额格式错误'})
-
- jieshao = request.data.get('jieshao', '').strip()
- xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip()
-
- # ---------- 4. 商品图片更新 ----------
- tupian_url = product.tupian_url
- tupian_file = request.FILES.get('tupian_file')
- if tupian_file:
- valid, msg = validate_image(tupian_file)
- if not valid:
- return Response({'code': 400, 'msg': f'商品图片无效:{msg}'})
- ext = tupian_file.name.split('.')[-1].lower()
- if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']:
- return Response({'code': 400, 'msg': '不支持的图片格式'})
- timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
- random_str = uuid.uuid4().hex[:8]
- new_filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}"
- uploaded_url = upload_to_oss(tupian_file, new_filename)
- if not uploaded_url:
- return Response({'code': 500, 'msg': '商品图片上传失败'})
- if product.tupian_url:
- delete_from_oss(product.tupian_url)
- tupian_url = new_filename
-
- # ---------- 5. 规则图片更新 ----------
- guize_tupian = product.guize_tupian
- guize_file = request.FILES.get('guize_file')
- if guize_file:
- valid, msg = validate_image(guize_file)
- if not valid:
- return Response({'code': 400, 'msg': f'规则图片无效:{msg}'})
- ext = guize_file.name.split('.')[-1].lower()
- if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']:
- return Response({'code': 400, 'msg': '不支持的图片格式'})
- timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
- random_str = uuid.uuid4().hex[:8]
- new_filename = f"shangpin/guizetupian/{timestamp}_{random_str}.{ext}"
- uploaded_url = upload_to_oss(guize_file, new_filename)
- if not uploaded_url:
- return Response({'code': 500, 'msg': '规则图片上传失败'})
- if product.guize_tupian:
- delete_from_oss(product.guize_tupian)
- guize_tupian = new_filename
-
- # ---------- 6. 更新数据库 ----------
- product.biaoqian = biaoqian
- product.jiage = jiage
- product.kucun = kucun
- product.duiwai_xiaoliang = duiwai_xiaoliang
- product.leixing_id = leixing_id
- product.zhuanqu_id = zhuanqu_id
- product.yaoqiuleixing = yaoqiuleixing
- product.huiyuan_id = huiyuan_id
- product.yongjin = yongjin
- product.kaioi_ewai_dashou_fencheng = kaioi_ewai
- product.ewai_dashou_fencheng = ewai_fencheng
- product.jieshao = jieshao
- product.xiadan_xuzhi = xiadan_xuzhi
- product.tupian_url = tupian_url
- product.guize_tupian = guize_tupian
- product.shenhezhuangtai = shenhezhuangtai
- product.save()
-
- return Response({'code': 0, 'msg': '商品修改成功', 'data': {'id': product.id}})
-
-
-
-
-
-class GetMemberListView(APIView):
- """
- 获取会员列表
- 权限:3300a
- POST /houtai/hthqhylb
- 参数:username
- 返回:code:0, data: { list: [...] }
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '3300a' not in permissions:
- return Response({'code': 403, 'msg': '无权限查看会员列表'})
-
- members = Huiyuan.objects.all().order_by('-create_time')
- member_list = []
- for m in members:
- member_list.append({
- 'huiyuan_id': m.huiyuan_id,
- 'jieshao': m.jieshao,
- 'jtjieshao': m.jtjieshao,
- 'jiage': str(m.jiage),
- 'guanshifc': str(m.guanshifc),
- 'zuzhangfc': str(m.zuzhangfc),
- 'goumai_cishu': m.goumai_cishu,
- 'create_time': m.create_time.isoformat() if m.create_time else None,
- 'update_time': m.update_time.isoformat() if m.update_time else None,
- })
- return Response({'code': 0, 'data': {'list': member_list}})
-
-
-class UpdateMemberView(APIView):
- """
- 修改会员信息
- 权限:3300a
- POST /houtai/htxghyxx
- 参数:
- username, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
- 返回:code:0 成功
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- huiyuan_id = request.data.get('huiyuan_id', '').strip()
- jieshao = request.data.get('jieshao', '').strip()
- jiage = request.data.get('jiage')
- guanshifc = request.data.get('guanshifc')
- zuzhangfc = request.data.get('zuzhangfc')
- jtjieshao = request.data.get('jtjieshao', '').strip()
-
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
- if not huiyuan_id:
- return Response({'code': 400, 'msg': '缺少会员ID'})
- if not jieshao:
- return Response({'code': 400, 'msg': '会员名称不能为空'})
-
- try:
- jiage = Decimal(str(jiage))
- guanshifc = Decimal(str(guanshifc))
- zuzhangfc = Decimal(str(zuzhangfc))
- if jiage < 0 or guanshifc < 0 or zuzhangfc < 0:
- raise ValueError
- except:
- return Response({'code': 400, 'msg': '金额格式错误'})
-
- if guanshifc + zuzhangfc > jiage:
- return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '3300a' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改会员'})
-
- with transaction.atomic():
- try:
- member = Huiyuan.objects.select_for_update().get(huiyuan_id=huiyuan_id)
- except Huiyuan.DoesNotExist:
- return Response({'code': 404, 'msg': '会员不存在'})
-
- member.jieshao = jieshao
- member.jiage = jiage
- member.guanshifc = guanshifc
- member.zuzhangfc = zuzhangfc
- member.jtjieshao = jtjieshao
- member.save()
-
- return Response({'code': 0, 'msg': '修改成功'})
-
-
-
-
-import random
-import string
-
-class AddMemberView(APIView):
- """
- 添加会员
- 权限:3300a
- POST /houtai/httjhy
- 参数:
- username, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
- 返回:code:0, data: { huiyuan_id }
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- jieshao = request.data.get('jieshao', '').strip()
- jiage = request.data.get('jiage')
- guanshifc = request.data.get('guanshifc')
- zuzhangfc = request.data.get('zuzhangfc')
- jtjieshao = request.data.get('jtjieshao', '').strip()
-
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
- if not jieshao:
- return Response({'code': 400, 'msg': '会员名称不能为空'})
-
- try:
- jiage = Decimal(str(jiage))
- guanshifc = Decimal(str(guanshifc))
- zuzhangfc = Decimal(str(zuzhangfc))
- if jiage <= 0 or guanshifc < 0 or zuzhangfc < 0:
- raise ValueError
- except:
- return Response({'code': 400, 'msg': '金额格式错误'})
-
- if guanshifc + zuzhangfc > jiage:
- return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '3300a' not in permissions:
- return Response({'code': 403, 'msg': '无权限添加会员'})
-
- # 生成唯一6位会员ID(数字+字母)
- def generate_huiyuan_id():
- while True:
- new_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
- if not Huiyuan.objects.filter(huiyuan_id=new_id).exists():
- return new_id
-
- with transaction.atomic():
- new_id = generate_huiyuan_id()
- member = Huiyuan.objects.create(
- huiyuan_id=new_id,
- jieshao=jieshao,
- jiage=jiage,
- guanshifc=guanshifc,
- zuzhangfc=zuzhangfc,
- jtjieshao=jtjieshao,
- goumai_cishu=0
- )
-
- return Response({'code': 0, 'msg': '添加成功', 'data': {'huiyuan_id': member.huiyuan_id}})
-
-
-
-
-
-class GetGuanliListView(APIView):
- """
- 获取管事列表
- 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一
- POST /houtai/hthqgslb
- 参数:
- username, keyword(可选), balance_op/balance_value, commission_op/commission_value,
- invite_count_op/invite_count_value, status, page, page_size
- 返回:
- code:0, data: { list: [...], total }
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
-
- # 权限校验(需要任一权限)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
- if not any(p in permissions for p in required_perms):
- return Response({'code': 403, 'msg': '您没有权限查看管事列表'})
-
- # 分页参数
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 20))
- if page < 1 or page_size < 1:
- raise ValueError
- except:
- return Response({'code': 400, 'msg': '分页参数错误'})
-
- # 筛选参数
- keyword = request.data.get('keyword', '').strip()
- balance_op = request.data.get('balance_op', '').strip().lower()
- balance_value = request.data.get('balance_value')
- commission_op = request.data.get('commission_op', '').strip().lower()
- commission_value = request.data.get('commission_value')
- invite_count_op = request.data.get('invite_count_op', '').strip().lower()
- invite_count_value = request.data.get('invite_count_value')
- status = request.data.get('status')
-
- # 构建查询:从 UserMain 开始,关联 UserGuanshi 和 UserBoss(左连接获取昵称)
- qs = UserMain.objects.filter(
- guanshi_profile__isnull=False # 确保是管事
- ).select_related('guanshi_profile').select_related('boss_profile')
-
- # 关键词搜索(用户ID 或 昵称来自 boss_profile.nickname)
- if keyword:
- qs = qs.filter(
- Q(yonghuid__icontains=keyword) |
- Q(boss_profile__nickname__icontains=keyword)
- )
-
- # 余额筛选(使用 guanshi_profile.yue)
- if balance_op and balance_value is not None and balance_value != '':
- try:
- balance_val = Decimal(str(balance_value))
- if balance_op == 'gte':
- qs = qs.filter(guanshi_profile__yue__gte=balance_val)
- elif balance_op == 'lte':
- qs = qs.filter(guanshi_profile__yue__lte=balance_val)
- else:
- return Response({'code': 400, 'msg': '余额比较符无效'})
- except:
- return Response({'code': 400, 'msg': '余额数值格式错误'})
-
- # 分佣总额筛选(guanshi_profile.chongzhifenrun)
- if commission_op and commission_value is not None and commission_value != '':
- try:
- commission_val = Decimal(str(commission_value))
- if commission_op == 'gte':
- qs = qs.filter(guanshi_profile__chongzhifenrun__gte=commission_val)
- elif commission_op == 'lte':
- qs = qs.filter(guanshi_profile__chongzhifenrun__lte=commission_val)
- else:
- return Response({'code': 400, 'msg': '分佣比较符无效'})
- except:
- return Response({'code': 400, 'msg': '分佣数值格式错误'})
-
- # 邀请打手总数筛选(guanshi_profile.yaogingshuliang)
- if invite_count_op and invite_count_value is not None and invite_count_value != '':
- try:
- invite_val = int(invite_count_value)
- if invite_count_op == 'gte':
- qs = qs.filter(guanshi_profile__yaogingshuliang__gte=invite_val)
- elif invite_count_op == 'lte':
- qs = qs.filter(guanshi_profile__yaogingshuliang__lte=invite_val)
- else:
- return Response({'code': 400, 'msg': '邀请数量比较符无效'})
- except:
- return Response({'code': 400, 'msg': '邀请数量格式错误'})
-
- # 状态筛选
- if status is not None and status != '':
- try:
- status_val = int(status)
- if status_val not in (0, 1):
- raise ValueError
- qs = qs.filter(guanshi_profile__zhuangtai=status_val)
- except:
- return Response({'code': 400, 'msg': '状态值无效'})
-
- # 计算总数
- total = qs.count()
-
- # 分页
- offset = (page - 1) * page_size
- users = qs.order_by('-create_time')[offset:offset + page_size]
-
- # 构建返回数据
- result = []
- for user in users:
- guanshi = user.guanshi_profile
- boss = user.boss_profile if hasattr(user, 'boss_profile') else None
- result.append({
- 'yonghuid': user.yonghuid,
- 'avatar': user.avatar or '',
- 'nickname': boss.nickname if boss else '',
- 'zhuangtai': guanshi.zhuangtai,
- 'yaogingshuliang': guanshi.yaogingshuliang,
- 'yue': str(guanshi.yue),
- 'chongzhifenrun': str(guanshi.chongzhifenrun),
- })
-
- return Response({'code': 0, 'data': {'list': result, 'total': total}})
-
-
-
-
-
-
-
-
-class GetGuanliDetailView(APIView):
- """
- 获取管事详情
- 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一
- POST /houtai/hthqgsxq
- 参数:username, yonghuid
- 返回:
- code:0, data: {
- base: {...},
- balance: {...},
- permanent_enabled, permanent_amount,
- custom_first_dividend: {},
- extra_dividend_map: {},
- all_members: []
- }
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- yonghuid = request.data.get('yonghuid', '').strip()
-
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
- if not yonghuid:
- return Response({'code': 400, 'msg': '缺少用户ID'})
-
- # 权限校验(需要任一权限)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
- if not any(p in permissions for p in required_perms):
- return Response({'code': 403, 'msg': '您没有权限查看管事详情'})
-
- # 查询用户主表和管事扩展表
- try:
- user = UserMain.objects.select_related('guanshi_profile', 'boss_profile').get(yonghuid=yonghuid)
- guanshi = user.guanshi_profile
- boss = user.boss_profile if hasattr(user, 'boss_profile') else None
- except UserMain.DoesNotExist:
- return Response({'code': 404, 'msg': '管事不存在'})
- except UserGuanshi.DoesNotExist:
- return Response({'code': 404, 'msg': '用户不是管事'})
-
- # 基础信息
- base_data = {
- 'yonghuid': user.yonghuid,
- 'nickname': boss.nickname if boss else '',
- 'avatar': user.avatar or '',
- 'dianhua': guanshi.dianhua or '',
- 'wechat': guanshi.wechat or '',
- 'zhuangtai': guanshi.zhuangtai,
- 'yaoqingma': guanshi.yaoqingma,
- 'yaoqingren': guanshi.yaoqingren,
- 'invite_qrcode_url': guanshi.invite_qrcode_url or '',
- 'yaogingshuliang': guanshi.yaogingshuliang,
- 'jinrichongzhi': guanshi.jinrichongzhi,
- 'jinyuechongzhi': guanshi.jinyuechongzhi,
- 'chongzhifenrun': str(guanshi.chongzhifenrun),
- 'create_time': guanshi.create_time.isoformat() if guanshi.create_time else None,
- }
-
- # 余额提现相关
- balance_data = {
- 'yue': str(guanshi.yue),
- 'jinritixian_jine': str(guanshi.jinritixian_jine),
- 'last_tixian_date': guanshi.last_tixian_date.isoformat() if guanshi.last_tixian_date else None,
- 'kaioi_ewai_xiane': guanshi.kaioi_ewai_xiane,
- 'ewai_xiane': str(guanshi.ewai_xiane),
- }
-
- # 永久分红(二次分红)
- permanent_enabled = guanshi.fenghong_erci_enabled
- permanent_amount = str(guanshi.fenghong_erci) if guanshi.fenghong_erci else '0'
-
- # 获取所有会员(用于前端选择)
- all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc'))
- for m in all_members:
- m['jiage'] = str(m['jiage'])
- m['guanshifc'] = str(m['guanshifc'])
-
- # 获取多次分红配置(按次数和会员组织)
- # 查询该管事的所有多次分红记录
- duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu')
- custom_first = {} # 首次分红定制(cishu=1)
- extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } }
-
- for record in duoci_records:
- # 获取会员信息
- member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first()
- if not member:
- continue
- member_info = {
- 'jieshao': member.jieshao,
- 'jiage': str(member.jiage),
- }
- if record.cishu == 1:
- custom_first[record.huiyuan] = float(record.guanshi_fenhong)
- else:
- times = record.cishu
- if times not in extra_map:
- extra_map[times] = {}
- extra_map[times][record.huiyuan] = {
- 'guanshi_fenhong': float(record.guanshi_fenhong),
- **member_info
- }
-
- # 注意:首次分红定制只存储有定制记录的会员,没有定制的会员使用默认值(前端通过会员表展示)
- # 额外次数分红按次数返回
-
- return Response({
- 'code': 0,
- 'data': {
- 'base': base_data,
- 'balance': balance_data,
- 'permanent_enabled': permanent_enabled,
- 'permanent_amount': permanent_amount,
- 'custom_first_dividend': custom_first,
- 'extra_dividend_map': extra_map,
- 'all_members': all_members,
- }
- })
-
-
-
-
-
-class UpdateGuanliView(APIView):
- """
- 修改管事信息(基础信息、余额、提现限额、分红策略)
- 权限细分:
- - 修改状态(封禁/解封)及昵称/电话/微信:需要4400a
- - 增加余额:需要4400c
- - 减少余额:需要4400b
- - 修改提现限额(kaioi_ewai_xiane/ewai_xiane):需要4400e
- - 修改分红相关(永久分红、定制首次、额外次数):需要4400f
- POST /houtai/htxggsxx
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- yonghuid = request.data.get('yonghuid', '').strip()
-
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
- if not yonghuid:
- return Response({'code': 400, 'msg': '缺少用户ID'})
-
- # 权限校验(先获取权限列表)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- # 查询管事对象(不在事务外加锁,事务内加锁)
- try:
- user = UserMain.objects.get(yonghuid=yonghuid)
- guanshi = user.guanshi_profile
- boss = user.boss_profile if hasattr(user, 'boss_profile') else None
- except UserMain.DoesNotExist:
- return Response({'code': 404, 'msg': '用户不存在'})
- except UserGuanshi.DoesNotExist:
- return Response({'code': 404, 'msg': '用户不是管事'})
-
- # 开始事务,内部使用 select_for_update
- with transaction.atomic():
- # 重新获取加锁对象
- user = UserMain.objects.select_for_update().get(yonghuid=yonghuid)
- guanshi = user.guanshi_profile
- boss = user.boss_profile if hasattr(user, 'boss_profile') else None
-
- # ---------- 1. 基础信息修改(需要4400a)----------
- nickname = request.data.get('nickname')
- dianhua = request.data.get('dianhua')
- wechat = request.data.get('wechat')
- zhuangtai = request.data.get('zhuangtai')
- avatar = request.data.get('avatar')
-
- if any(v is not None for v in [nickname, dianhua, wechat, zhuangtai, avatar]):
- if '4400a' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改管事基础信息(需要4400a)'})
- # 处理昵称(在老板扩展表中)
- if nickname is not None:
- if not boss:
- boss = UserBoss.objects.create(user=user, nickname=nickname)
- else:
- boss.nickname = nickname
- boss.save()
- # 处理其他字段
- if dianhua is not None:
- guanshi.dianhua = dianhua
- if wechat is not None:
- guanshi.wechat = wechat
- if zhuangtai is not None:
- try:
- zt = int(zhuangtai)
- if zt in (0, 1):
- guanshi.zhuangtai = zt
- except:
- pass
- if avatar is not None:
- user.avatar = avatar
- user.save()
- guanshi.save()
-
- # ---------- 2. 余额修改(加:4400c;减:4400b)----------
- new_yue = request.data.get('yue')
- if new_yue is not None:
- try:
- new_yue = Decimal(str(new_yue))
- if new_yue < 0:
- return Response({'code': 400, 'msg': '余额不能为负数'})
- except:
- return Response({'code': 400, 'msg': '余额格式错误'})
-
- old_yue = guanshi.yue
- if new_yue > old_yue:
- if '4400c' not in permissions:
- return Response({'code': 403, 'msg': '无权限增加余额(需要4400c)'})
- elif new_yue < old_yue:
- if '4400b' not in permissions:
- return Response({'code': 403, 'msg': '无权限减少余额(需要4400b)'})
- guanshi.yue = new_yue
- guanshi.save()
-
- # ---------- 3. 提现限额修改(需要4400e)----------
- kaioi_ewai = request.data.get('kaioi_ewai_xiane')
- ewai_val = request.data.get('ewai_xiane')
- if kaioi_ewai is not None or ewai_val is not None:
- if '4400e' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改提现限额(需要4400e)'})
- if kaioi_ewai is not None:
- guanshi.kaioi_ewai_xiane = bool(kaioi_ewai)
- if ewai_val is not None:
- try:
- guanshi.ewai_xiane = Decimal(str(ewai_val))
- except:
- return Response({'code': 400, 'msg': '额外限额金额格式错误'})
- guanshi.save()
-
- # ---------- 4. 分红相关修改(需要4400f)----------
- perm_enabled = request.data.get('permanent_enabled')
- perm_amount = request.data.get('permanent_amount')
- custom_first = request.data.get('custom_first_dividend')
- extra_map = request.data.get('extra_dividend_map')
-
- if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
- if '4400f' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改分红策略(需要4400f)'})
-
- # 4.1 永久分红
- if perm_enabled is not None:
- guanshi.fenghong_erci_enabled = bool(perm_enabled)
- if perm_amount is not None:
- try:
- guanshi.fenghong_erci = Decimal(str(perm_amount))
- except:
- return Response({'code': 400, 'msg': '永久分红金额格式错误'})
- guanshi.save()
-
- # 4.2 首次定制分红(cishu=1)
- if custom_first is not None and isinstance(custom_first, dict):
- # 获取当前所有首次定制记录
- existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1)
- # 前端传来的定制字典 {会员ID: 金额}
- for huiyuan_id, amount in custom_first.items():
- try:
- amount = Decimal(str(amount))
- except:
- continue
- if amount <= 0:
- # 金额为0或负数表示删除定制
- DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete()
- else:
- DuociFenhong.objects.update_or_create(
- yonghuid=yonghuid,
- huiyuan=huiyuan_id,
- cishu=1,
- defaults={
- 'guanshi_fenhong': amount,
- 'zuzhang_fenhong': Decimal('0')
- }
- )
- # 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
- for record in existing_first:
- if record.huiyuan not in custom_first:
- record.delete()
-
- # 4.3 额外次数分红(cishu >= 2)
- if extra_map is not None and isinstance(extra_map, dict):
- # 收集需要保留的记录
- keep_set = set()
- for times_str, members in extra_map.items():
- try:
- cishu = int(times_str)
- if cishu < 2:
- continue
- except:
- continue
- if not isinstance(members, dict):
- continue
- for huiyuan_id, data in members.items():
- if not isinstance(data, dict):
- continue
- amount = data.get('guanshi_fenhong')
- if amount is None:
- continue
- try:
- amount = Decimal(str(amount))
- except:
- continue
- keep_set.add((cishu, huiyuan_id))
- # 更新或创建
- DuociFenhong.objects.update_or_create(
- yonghuid=yonghuid,
- huiyuan=huiyuan_id,
- cishu=cishu,
- defaults={
- 'guanshi_fenhong': amount,
- 'zuzhang_fenhong': Decimal('0')
- }
- )
- # 删除不在 keep_set 中的额外次数记录
- existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2)
- for record in existing_extra:
- if (record.cishu, record.huiyuan) not in keep_set:
- record.delete()
-
- return Response({'code': 0, 'msg': '修改成功'})
-
-
-
-
-
-
-
-
-
-class GetWithdrawSettingsView(APIView):
- """
- 获取提现设置(手续费利率、每日限额、当日已提现总额)
- 权限:需要 5500a 或 5500b 或 5500c 任一
- POST /houtai/hthqtxpz
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- # 需要5500a/b/c任一
- if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
- return Response({'code': 403, 'msg': '无权限查看提现设置'})
-
- # 1. 手续费利率(Lilubiao:5打手/6管事/8组长/9审核官/10商家/11打手押金)
- rate_map = {}
- for leixing, code in [(1, '5'), (2, '6'), (3, '8'), (4, '9'), (5, '11'), (6, '10')]:
- obj = Lilubiao.objects.filter(fadanpingtai=code).first()
- rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0
-
- # 1b. 订单/押金分红费率(Lilubiao:1平台订单/3商家订单/12押金管事/13商家订单管事分红)
- order_rate_map = {}
- for leixing, code in [(1, '1'), (3, '3'), (12, '12'), (13, '13')]:
- obj = Lilubiao.objects.filter(fadanpingtai=code).first()
- order_rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0
-
- # 2. 个人每日限额 (leixing=1,2,3)
- quota_map = {}
- for leixing in [1, 2, 3]:
- obj = TixianQuotaDefault.objects.filter(leixing=leixing).first()
- quota_map[leixing] = float(obj.default_quota) if obj else 20.0
-
- # 3. 每日总限额(提现类型1~6 → 配置表4~9)
- total_limit_map = {}
- for withdraw_leixing, quota_leixing in [(1, 4), (2, 5), (3, 6), (4, 7), (5, 8), (6, 9)]:
- obj = TixianQuotaDefault.objects.filter(leixing=quota_leixing).first()
- total_limit_map[withdraw_leixing] = float(obj.default_quota) if obj else 0.0
-
- # 4. 当日已提现总额(按提现类型 leixing 1~6)
- today = date.today()
- today_totals = {i: 0.0 for i in range(1, 7)}
- records = TixianRiTongji.objects.filter(riqi=today)
- for rec in records:
- if rec.leixing in today_totals:
- today_totals[rec.leixing] = float(rec.total_amount)
-
- # 🆕 获取提现模式(自动1 / 手动2),默认2
- from peizhi.models import WithdrawConfig # 确保导入模型
- config = WithdrawConfig.objects.filter(id=1).first()
- withdraw_mode = config.mode if config else 2
-
- return Response({
- 'code': 0,
- 'data': {
- 'withdraw_mode': withdraw_mode,
- 'rates': rate_map,
- 'order_rates': order_rate_map,
- 'quotas': quota_map,
- 'total_limits': total_limit_map,
- 'today_totals': today_totals,
- }
- })
-
-
-class UpdateWithdrawSettingsView(APIView):
- """
- 修改提现设置(权限细分):
- - 修改打手相关(利率、个人限额、总限额)需要5500a
- - 修改管事相关需要5500b
- - 修改组长相关需要5500c
- 注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算),
- 后端不直接修改 TixianRiTongji,该表由提现流程自动累加。
- POST /houtai/htxgtxsz
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- # 权限映射:要修改的角色需要对应的权限
- # 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3
- rates = request.data.get('rates', {})
- order_rates = request.data.get('order_rates', request.data.get('commission_rates', {}))
- quotas = request.data.get('quotas', {})
- total_limits = request.data.get('total_limits', {})
-
- # 角色类型映射
- role_perms = {1: '5500a', 2: '5500b', 3: '5500c'}
- # 利率代码映射(1-3 有水缸限额;4审核官/5打手押金/6商家余额仅费率)
- rate_code_map = {1: '5', 2: '6', 3: '8', 4: '9', 5: '11', 6: '10'}
- # 订单/押金分红(1平台订单/3商家订单/12押金管事/13商家订单管事分红)
- order_rate_code_map = {1: '1', 3: '3', 12: '12', 13: '13'}
- # 总限额:提现类型 → TixianQuotaDefault.leixing(4~9)
- total_code_map = {1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9}
-
- def _rate_key_present(data, role):
- return (str(role) in data and data[str(role)] is not None) or \
- (role in data and data[role] is not None)
-
- with transaction.atomic():
- # 遍历三个角色(限额相关)
- for role in [1, 2, 3]:
- perm_needed = role_perms[role]
- if perm_needed not in permissions:
- if _rate_key_present(rates, role) or \
- (str(role) in quotas and quotas[str(role)] is not None) or \
- (role in quotas and quotas.get(role) is not None) or \
- (str(role) in total_limits and total_limits[str(role)] is not None) or \
- (role in total_limits and total_limits.get(role) is not None):
- return Response({'code': 403, 'msg': f'无权限修改{["","打手","管事","组长"][role]}相关设置(需要{perm_needed})'})
-
- # 其他提现手续费(审核官/打手押金/商家余额):需 5500a/b/c 任一
- extra_role_names = {4: '考核官', 5: '打手押金', 6: '商家余额'}
- for role in [4, 5, 6]:
- if _rate_key_present(rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')):
- return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}提现手续费率'})
- if (str(role) in total_limits and total_limits[str(role)] is not None) or \
- (role in total_limits and total_limits.get(role) is not None):
- if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
- return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}总限额'})
-
- # 订单/押金分红费率:需 5500a/b/c 任一
- for role in [1, 3, 12, 13]:
- if _rate_key_present(order_rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')):
- order_names = {
- 1: '平台订单打手分红', 3: '商家订单打手分红',
- 12: '押金管事分红', 13: '管事订单分红',
- }
- return Response({'code': 403, 'msg': f'无权限修改{order_names[role]}费率'})
-
- # 1. 修改手续费利率
- for role in [1, 2, 3, 4, 5, 6]:
- val = rates.get(str(role), rates.get(role))
- if val is not None:
- rate = Decimal(str(val))
- code = rate_code_map[role]
- obj, created = Lilubiao.objects.select_for_update().get_or_create(
- fadanpingtai=code,
- defaults={'lilu': rate}
- )
- if not created:
- obj.lilu = rate
- obj.save()
-
- # 1b. 修改订单/押金分红费率
- for role in [1, 3, 12, 13]:
- val = order_rates.get(str(role), order_rates.get(role))
- if val is not None:
- rate = Decimal(str(val))
- code = order_rate_code_map[role]
- obj, created = Lilubiao.objects.select_for_update().get_or_create(
- fadanpingtai=code,
- defaults={'lilu': rate}
- )
- if not created:
- obj.lilu = rate
- obj.save()
-
- # 2. 修改个人每日限额
- for role in [1, 2, 3]:
- if str(role) in quotas:
- val = quotas[str(role)]
- if val is not None:
- quota = Decimal(str(val))
- obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create(
- leixing=role,
- defaults={'default_quota': quota}
- )
- if not created:
- obj.default_quota = quota
- obj.save()
-
- # 3. 修改每日总限额(1打手/2管事/3组长/4考核官/5押金/6商家)
- for role in [1, 2, 3, 4, 5, 6]:
- val = total_limits.get(str(role), total_limits.get(role))
- if val is not None:
- limit = Decimal(str(val))
- code = total_code_map[role]
- obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create(
- leixing=code,
- defaults={'default_quota': limit}
- )
- if not created:
- obj.default_quota = limit
- obj.save()
-
- # 🆕 修改提现模式(有5500a/b/c任一即可)
- from peizhi.models import WithdrawConfig
- withdraw_mode = request.data.get('withdraw_mode')
- if withdraw_mode is not None:
- if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
- return Response({'code': 403, 'msg': '无权限修改提现模式'})
- # 更新或创建 id=1 的配置
- WithdrawConfig.objects.update_or_create(
- id=1,
- defaults={'mode': int(withdraw_mode)}
- )
-
- # 注意:TixianRiTongji 表由提现流程自动更新,此处不直接修改
- return Response({'code': 0, 'msg': '设置修改成功'})
-
-
-
-class GetZuzhangListView(APIView):
- """
- 组长列表接口(分页 + 多条件筛选)
- 路由:POST /houtai/hthqzzlb
- 权限:需要登录,且拥有 6600a~6600e 任一权限
- """
- permission_classes = [IsAuthenticated]
-
- @transaction.atomic # 保证查询的一致性(可选,对只读查询影响不大但无坏处)
- def post(self, request):
- # 1. 获取前端传递的 username(用于防越权)
- frontend_username = request.data.get('username', '')
-
- # 2. 调用公共校验方法,验证身份并获取权限列表
- result = verify_kefu_permission(request, username_from_frontend=frontend_username)
-
- # 重要:verify_kefu_permission 返回格式为 (user_obj, permissions_list) 或 (None, Response)
- # 如果第二个元素是 Response 对象,说明校验失败,直接返回该 Response
- if isinstance(result, tuple) and len(result) == 2:
- user_obj, second = result
- if isinstance(second, Response):
- return second # 校验失败,直接返回错误响应
- else:
- permissions = second # 正常情况:permissions 是列表
- else:
- # 防止意外返回格式,视为失败
- return Response({'code': 500, 'msg': '权限校验模块异常'})
-
- # 3. 检查是否拥有组长管理所需权限(6600a ~ 6600e 任一)
- required_perms = ['6600a', '6600b', '6600c', '6600d', '6600e']
- if not any(perm in permissions for perm in required_perms):
- logger.warning(f"用户 {request.user.phone} 尝试访问组长列表但缺少必要权限,已有权限: {permissions}")
- return Response({'code': 403, 'msg': '您无权访问组长管理页面'})
-
- # 4. 提取请求参数(分页 + 筛选)
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 20))
- except ValueError:
- page, page_size = 1, 20
-
- keyword = request.data.get('keyword', '').strip()
- status = request.data.get('status') # 1=正常, 0=禁用
- invite_count_op = request.data.get('invite_count_op') # gte / lte
- invite_count_value = request.data.get('invite_count_value')
- amount_op = request.data.get('amount_op')
- amount_value = request.data.get('amount_value')
- commission_op = request.data.get('commission_op')
- commission_value = request.data.get('commission_value')
-
- # 5. 构建基础 QuerySet(预加载关联表)
- queryset = UserZuzhang.objects.select_related('user', 'user__boss_profile').all()
-
- # 6. 应用筛选条件(使用 ORM 安全过滤)
- if keyword:
- queryset = queryset.filter(
- Q(user__yonghuid__icontains=keyword) |
- Q(user__boss_profile__nickname__icontains=keyword)
- )
-
- if status is not None and status != '':
- try:
- queryset = queryset.filter(zhuangtai=int(status))
- except ValueError:
- pass
-
- # 邀请总数筛选
- if invite_count_value not in (None, ''):
- try:
- val = float(invite_count_value)
- op = invite_count_op if invite_count_op in ('gte', 'lte') else 'gte'
- queryset = queryset.filter(**{f'yaoqing_zongshu__{op}': val})
- except (ValueError, TypeError):
- pass
-
- # 可提现金额筛选
- if amount_value not in (None, ''):
- try:
- val = float(amount_value)
- op = amount_op if amount_op in ('gte', 'lte') else 'gte'
- queryset = queryset.filter(**{f'ketixian_jine__{op}': val})
- except (ValueError, TypeError):
- pass
-
- # 分佣总额筛选
- if commission_value not in (None, ''):
- try:
- val = float(commission_value)
- op = commission_op if commission_op in ('gte', 'lte') else 'gte'
- queryset = queryset.filter(**{f'fenyong_zonge__{op}': val})
- except (ValueError, TypeError):
- pass
-
- # 7. 总数统计
- total = queryset.count()
-
- # 8. 分页
- offset = (page - 1) * page_size
- paged_queryset = queryset[offset:offset + page_size]
-
- # 9. 构造返回数据
- data_list = []
- for zu in paged_queryset:
- user = zu.user
- nickname = ''
- if hasattr(user, 'boss_profile') and user.boss_profile:
- nickname = user.boss_profile.nickname or ''
-
- data_list.append({
- 'yonghuid': user.yonghuid,
- 'avatar': user.avatar or '',
- 'nickname': nickname,
- 'fenyong_zonge': float(zu.fenyong_zonge),
- 'ketixian_jine': float(zu.ketixian_jine),
- 'yaoqing_zongshu': zu.yaoqing_zongshu,
- 'zhuangtai': zu.zhuangtai,
- })
-
- return Response({
- 'code': 0,
- 'msg': 'success',
- 'data': {
- 'list': data_list,
- 'total': total,
- }
- })
-
-
-
-
-
-
-
-
-
-
-class GetZuzhangDetailView(APIView):
- """
- 获取组长详情
- 权限:6600a,6600b,6600c,6600d,6600e 任一
- POST /houtai/hthqzzxq
- 参数:username, yonghuid
- 返回:
- code:0, data: {
- base: {...},
- balance: {...},
- permanent_enabled, permanent_amount,
- custom_first_dividend: {},
- extra_dividend_map: {},
- all_members: []
- }
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- yonghuid = request.data.get('yonghuid', '').strip()
-
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
- if not yonghuid:
- return Response({'code': 400, 'msg': '缺少用户ID'})
-
- # 权限校验(需要任一权限)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- required_perms = {'6600a', '6600b', '6600c', '6600d', '6600e'}
- if not any(p in permissions for p in required_perms):
- return Response({'code': 403, 'msg': '您没有权限查看组长详情'})
-
- # 查询组长主表、扩展表、老板扩展表(昵称)
- try:
- user = UserMain.objects.select_related('zuzhang_profile', 'boss_profile').get(yonghuid=yonghuid)
- zuzhang = user.zuzhang_profile
- boss = user.boss_profile if hasattr(user, 'boss_profile') else None
- except UserMain.DoesNotExist:
- return Response({'code': 404, 'msg': '组长不存在'})
- except UserZuzhang.DoesNotExist:
- return Response({'code': 404, 'msg': '用户不是组长'})
-
- # 基础信息
- base_data = {
- 'yonghuid': user.yonghuid,
- 'nickname': boss.nickname if boss else '',
- 'avatar': user.avatar or '',
- 'dianhua': '', # 组长表无电话字段?组长扩展表没有电话/微信,但前端可能需要,可从UserMain关联?实际上组长表无电话字段,这里留空或从其他表获取
- 'wechat': '',
- 'zhuangtai': zuzhang.zhuangtai,
- 'yaoqingma': zuzhang.yaoqingma,
- 'yaoqingren': '', # 组长表没有邀请人字段
- 'haibao_url': zuzhang.haibao_url or '',
- 'yaoqing_zongshu': zuzhang.yaoqing_zongshu,
- 'jinri_fenyong': str(zuzhang.jinri_fenyong),
- 'jinyue_fenyong': str(zuzhang.jinyue_fenyong),
- 'fenyong_zonge': str(zuzhang.fenyong_zonge),
- 'create_time': zuzhang.create_time.isoformat() if zuzhang.create_time else None,
- }
-
- # 余额提现相关
- balance_data = {
- 'ketixian_jine': str(zuzhang.ketixian_jine),
- 'jinri_tixian': str(zuzhang.jinri_tixian),
- 'last_tixian_time': zuzhang.last_tixian_time.isoformat() if zuzhang.last_tixian_time else None,
- 'kaioi_ewai_tixian': zuzhang.kaioi_ewai_tixian,
- 'ewai_tixian_xiane': str(zuzhang.ewai_tixian_xiane),
- }
-
- # 永久分红(额外分红开关和金额)
- permanent_enabled = zuzhang.kaioi_ewai_fenhong
- permanent_amount = str(zuzhang.ewai_fenhong_jine) if zuzhang.ewai_fenhong_jine else '0'
-
- # 获取所有会员(用于前端选择)
- all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc'))
- for m in all_members:
- m['jiage'] = str(m['jiage'])
- m['zuzhangfc'] = str(m['zuzhangfc'])
-
- # 获取多次分红配置(按次数和会员组织)
- duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu')
- custom_first = {} # 首次分红定制(cishu=1)
- extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } }
-
- for record in duoci_records:
- # 获取会员信息
- member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first()
- if not member:
- continue
- member_info = {
- 'jieshao': member.jieshao,
- 'jiage': str(member.jiage),
- }
- if record.cishu == 1:
- custom_first[record.huiyuan] = float(record.zuzhang_fenhong)
- else:
- times = record.cishu
- if times not in extra_map:
- extra_map[times] = {}
- extra_map[times][record.huiyuan] = {
- 'zuzhang_fenhong': float(record.zuzhang_fenhong),
- **member_info
- }
-
- return Response({
- 'code': 0,
- 'data': {
- 'base': base_data,
- 'balance': balance_data,
- 'permanent_enabled': permanent_enabled,
- 'permanent_amount': permanent_amount,
- 'custom_first_dividend': custom_first,
- 'extra_dividend_map': extra_map,
- 'all_members': all_members,
- }
- })
-
-
-
-
-class UpdateZuzhangView(APIView):
- """
- 修改组长信息(基础信息、余额、提现限额、分红策略)
- 权限细分:
- - 增加可提现金额:6600a
- - 减少可提现金额:6600b
- - 修改状态(封禁/解封):6600c
- - 修改提现限额(kaioi_ewai_tixian/ewai_tixian_xiane):6600d
- - 修改分红相关(永久分红、定制首次、额外次数):6600e
- POST /houtai/htxgzzxx
- 参数:
- username, yonghuid,
- nickname, avatar, dianhua, wechat, zhuangtai, # 基础
- ketixian_jine, kaioi_ewai_tixian, ewai_tixian_xiane, # 余额提现
- permanent_enabled, permanent_amount, # 永久分红
- custom_first_dividend: {}, # 首次定制分红 {会员ID: 金额}
- extra_dividend_map: {} # 额外次数分红 {次数: {会员ID: {zuzhang_fenhong}}}
- """
- permission_classes = []
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- yonghuid = request.data.get('yonghuid', '').strip()
-
- if not username:
- return Response({'code': 401, 'msg': '缺少username'})
- if not yonghuid:
- return Response({'code': 400, 'msg': '缺少用户ID'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- # 查询组长对象
- try:
- user = UserMain.objects.get(yonghuid=yonghuid)
- zuzhang = user.zuzhang_profile
- boss = user.boss_profile if hasattr(user, 'boss_profile') else None
- except UserMain.DoesNotExist:
- return Response({'code': 404, 'msg': '用户不存在'})
- except UserZuzhang.DoesNotExist:
- return Response({'code': 404, 'msg': '用户不是组长'})
-
- # 获取原始数据(用于对比金额变化)
- old_ketixian_jine = zuzhang.ketixian_jine
-
- # 开始事务
- with transaction.atomic():
- # ---------- 1. 基础信息修改(需要6600c?按需求,基础信息修改通常用6600c,但为了安全,暂不校验,如有需要可加)----------
- nickname = request.data.get('nickname', '').strip()
- avatar = request.data.get('avatar', '').strip()
- dianhua = request.data.get('dianhua', '').strip()
- wechat = request.data.get('wechat', '').strip()
- zhuangtai = request.data.get('zhuangtai')
- # 状态修改需要6600c
- if zhuangtai is not None:
- if '6600c' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改组长状态(需要6600c)'})
- try:
- zt = int(zhuangtai)
- if zt in (0, 1):
- zuzhang.zhuangtai = zt
- except:
- pass
- if nickname:
- if not boss:
- boss = UserBoss.objects.create(user=user, nickname=nickname)
- else:
- boss.nickname = nickname
- boss.save()
- if avatar:
- user.avatar = avatar
- user.save()
- # 组长表没有电话/微信字段,如需存储可暂存到UserMain或忽略
- # 这里不处理
-
- # ---------- 2. 可提现金额修改(加:6600a;减:6600b)----------
- new_ketixian_jine = request.data.get('ketixian_jine')
- if new_ketixian_jine is not None:
- try:
- new_val = Decimal(str(new_ketixian_jine))
- if new_val < 0:
- return Response({'code': 400, 'msg': '可提现金额不能为负数'})
- except:
- return Response({'code': 400, 'msg': '金额格式错误'})
- if new_val > old_ketixian_jine:
- if '6600a' not in permissions:
- return Response({'code': 403, 'msg': '无权限增加可提现金额(需要6600a)'})
- elif new_val < old_ketixian_jine:
- if '6600b' not in permissions:
- return Response({'code': 403, 'msg': '无权限减少可提现金额(需要6600b)'})
- zuzhang.ketixian_jine = new_val
- zuzhang.save(update_fields=['ketixian_jine'])
-
- # ---------- 3. 提现限额修改(需要6600d)----------
- kaioi_ewai = request.data.get('kaioi_ewai_tixian')
- ewai_val = request.data.get('ewai_tixian_xiane')
- if kaioi_ewai is not None or ewai_val is not None:
- if '6600d' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改提现限额(需要6600d)'})
- if kaioi_ewai is not None:
- zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai)
- if ewai_val is not None:
- try:
- zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val))
- except:
- return Response({'code': 400, 'msg': '额外限额金额格式错误'})
- zuzhang.save()
-
- # ---------- 4. 分红相关修改(需要6600e)----------
- perm_enabled = request.data.get('permanent_enabled')
- perm_amount = request.data.get('permanent_amount')
- custom_first = request.data.get('custom_first_dividend')
- extra_map = request.data.get('extra_dividend_map')
-
- if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
- if '6600e' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改分红策略(需要6600e)'})
-
- # 4.1 永久分红(额外分红开关和金额)
- if perm_enabled is not None:
- zuzhang.kaioi_ewai_fenhong = bool(perm_enabled)
- if perm_amount is not None:
- try:
- zuzhang.ewai_fenhong_jine = Decimal(str(perm_amount))
- except:
- return Response({'code': 400, 'msg': '永久分红金额格式错误'})
- zuzhang.save()
-
- # 4.2 首次定制分红(cishu=1)
- if custom_first is not None and isinstance(custom_first, dict):
- existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1)
- for huiyuan_id, amount in custom_first.items():
- try:
- amount = Decimal(str(amount))
- except:
- continue
- if amount <= 0:
- # 删除定制(恢复默认)
- DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete()
- else:
- DuociFenhong.objects.update_or_create(
- yonghuid=yonghuid,
- huiyuan=huiyuan_id,
- cishu=1,
- defaults={
- 'zuzhang_fenhong': amount,
- 'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红
- }
- )
- # 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
- for record in existing_first:
- if record.huiyuan not in custom_first:
- record.delete()
-
- # 4.3 额外次数分红(cishu >= 2)
- if extra_map is not None and isinstance(extra_map, dict):
- keep_set = set()
- for times_str, members in extra_map.items():
- try:
- cishu = int(times_str)
- if cishu < 2:
- continue
- except:
- continue
- if not isinstance(members, dict):
- continue
- for huiyuan_id, data in members.items():
- if not isinstance(data, dict):
- continue
- amount = data.get('zuzhang_fenhong')
- if amount is None:
- continue
- try:
- amount = Decimal(str(amount))
- except:
- continue
- keep_set.add((cishu, huiyuan_id))
- DuociFenhong.objects.update_or_create(
- yonghuid=yonghuid,
- huiyuan=huiyuan_id,
- cishu=cishu,
- defaults={
- 'zuzhang_fenhong': amount,
- 'guanshi_fenhong': Decimal('0')
- }
- )
- # 删除不在 keep_set 中的额外次数记录
- existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2)
- for record in existing_extra:
- if (record.cishu, record.huiyuan) not in keep_set:
- record.delete()
-
- # 保存组长其他可能修改的字段(例如电话、微信没有,忽略)
-
- return Response({'code': 0, 'msg': '修改成功'})
-
-
-
-
-class GetProductTypeZoneView(APIView):
- """
- 获取商品类型、专区及会员列表
- 权限:需要拥有 7007a
- 请求:POST /houtai/hthqsplxzq
- 参数:{"username": "客服手机号"}
- 返回:
- code: 0
- data: {
- type_list: [{id, jieshao, tupian_url, yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, is_cross_enabled, paixu, product_count}, ...],
- zone_list: [{id, mingzi, leixing_id, shenhezhuangtai, paixu}, ...],
- member_list: [{huiyuan_id, jieshao, jiage}, ...]
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '7007a' not in permissions:
- return Response({'code': 403, 'msg': '您无权访问此页面'})
-
- # 查询所有商品类型(含专区数量)
- types = ShangpinLeixing.objects.all().order_by('-paixu') # paixu 越大越靠前
- type_list = []
- for t in types:
- # 统计该类型下的专区数量
- zone_count = ShangpinZhuanqu.objects.filter(leixing_id=t.id).count()
- type_list.append({
- 'id': t.id,
- 'jieshao': t.jieshao or '',
- 'tupian_url': t.tupian_url or '',
- 'yaoqiuleixing': t.yaoqiuleixing,
- 'huiyuan_id': t.huiyuan_id or '',
- 'yongjin': float(t.yongjin) if t.yongjin is not None else None,
- 'shenhezhuangtai': t.shenhezhuangtai,
- 'is_cross_enabled': t.is_cross_enabled,
- 'paixu': t.paixu,
- 'product_count': zone_count,
- })
-
- # 查询所有专区
- zones = ShangpinZhuanqu.objects.all().order_by('-paixu')
- zone_list = [
- {
- 'id': z.id,
- 'mingzi': z.mingzi or '',
- 'leixing_id': z.leixing_id,
- 'shenhezhuangtai': z.shenhezhuangtai,
- 'paixu': z.paixu,
- }
- for z in zones
- ]
-
- # 查询所有会员(只返回 id、介绍、价格)
- members = Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage')
- member_list = [
- {
- 'huiyuan_id': m['huiyuan_id'],
- 'jieshao': m['jieshao'],
- 'jiage': float(m['jiage']),
- }
- for m in members
- ]
-
- return Response({
- 'code': 0,
- 'data': {
- 'type_list': type_list,
- 'zone_list': zone_list,
- 'member_list': member_list,
- }
- })
-
-
-
-
-
-
-
-
-'''class ModifyProductTypeZoneView(APIView):
- """
- 统一修改商品类型/专区(增、删、改)
- 权限:需要拥有 7007a
- 请求:POST /houtai/htxgsplx
- 支持 multipart/form-data 和 application/json
- 参数(通用):
- username: 客服账号
- type: 'leixing' 或 'zhuanqu'
- action: 'add' / 'update' / 'delete'
- 类型专用参数:
- - add/update: id (update时必填), jieshao, tupian_url(可不上传), file(图片文件), yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, is_cross_enabled, sync_to_products (可选)
- - delete: id, delete_products (布尔)
- 专区专用参数:
- - add/update: id (update时必填), mingzi, leixing_id, shenhezhuangtai
- - delete: id, delete_products (布尔)
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser, MultiPartParser, FormParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- obj_type = request.data.get('type') # 'leixing' 或 'zhuanqu'
- action = request.data.get('action') # 'add', 'update', 'delete'
-
- if not username or not obj_type or not action:
- return Response({'code': 400, 'msg': '缺少必要参数'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '7007a' not in permissions:
- return Response({'code': 403, 'msg': '您无权进行此操作'})
-
- if obj_type == 'leixing':
- return self.handle_leixing(request, action)
- elif obj_type == 'zhuanqu':
- return self.handle_zhuanqu(request, action)
- else:
- return Response({'code': 400, 'msg': '无效的type参数'})
-
- # ---------- 商品类型处理 ----------
- def handle_leixing(self, request, action):
- if action == 'add':
- return self.add_leixing(request)
- elif action == 'update':
- return self.update_leixing(request)
- elif action == 'delete':
- return self.delete_leixing(request)
- else:
- return Response({'code': 400, 'msg': '无效的action'})
-
- def add_leixing(self, request):
- # 获取表单字段
- jieshao = request.data.get('jieshao', '').strip()
- yaoqiuleixing = request.data.get('yaoqiuleixing')
- huiyuan_id = request.data.get('huiyuan_id', '').strip() or None
- yongjin = request.data.get('yongjin')
- shenhezhuangtai = request.data.get('shenhezhuangtai', 1)
- is_cross_enabled = request.data.get('is_cross_enabled', False)
- image_file = request.FILES.get('file') # 上传的图片文件
-
- if not jieshao:
- return Response({'code': 400, 'msg': '类型名称不能为空'})
- if yaoqiuleixing not in [1, 2]:
- return Response({'code': 400, 'msg': '抢单要求类型无效'})
- if not image_file:
- return Response({'code': 400, 'msg': '请上传类型图片'})
-
- # 校验图片
- try:
- from utils.oss_utils import validate_image
- valid, msg = validate_image(image_file)
- if not valid:
- return Response({'code': 400, 'msg': msg})
- except ImportError:
- pass
-
- # 上传图片到OSS
- ext = image_file.name.split('.')[-1].lower()
- new_filename = f"{uuid.uuid4().hex}.{ext}"
- oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}"
- url = upload_to_oss(image_file, oss_path)
- if not url:
- return Response({'code': 500, 'msg': '图片上传失败'})
-
- # 创建类型
- try:
- with transaction.atomic():
- leixing = ShangpinLeixing.objects.create(
- jieshao=jieshao,
- tupian_url=oss_path, # 存储相对路径
- yaoqiuleixing=yaoqiuleixing,
- huiyuan_id=huiyuan_id,
- yongjin=Decimal(yongjin) if yongjin else None,
- shenhezhuangtai=int(shenhezhuangtai),
- is_cross_enabled=is_cross_enabled in [True, 'true', '1'],
- paixu=0 # 新添加的类型排序默认0,前端可再调整
- )
- return Response({'code': 0, 'msg': '添加成功', 'data': {'id': leixing.id}})
- except Exception as e:
- logger.error(f"添加类型失败: {e}", exc_info=True)
- # 删除已上传的图片
- delete_from_oss(oss_path)
- return Response({'code': 500, 'msg': '数据库错误'})
-
- def update_leixing(self, request):
- type_id = request.data.get('id')
- if not type_id:
- return Response({'code': 400, 'msg': '缺少类型ID'})
- try:
- leixing = ShangpinLeixing.objects.get(id=type_id)
- except ShangpinLeixing.DoesNotExist:
- return Response({'code': 404, 'msg': '类型不存在'})
-
- # 收集修改字段
- changes = {}
- jieshao = request.data.get('jieshao')
- if jieshao is not None and jieshao != leixing.jieshao:
- changes['jieshao'] = jieshao.strip()
-
- yaoqiuleixing = request.data.get('yaoqiuleixing')
- if yaoqiuleixing is not None and int(yaoqiuleixing) != leixing.yaoqiuleixing:
- changes['yaoqiuleixing'] = int(yaoqiuleixing)
-
- huiyuan_id = request.data.get('huiyuan_id', '').strip() or None
- if huiyuan_id != leixing.huiyuan_id:
- changes['huiyuan_id'] = huiyuan_id
-
- yongjin = request.data.get('yongjin')
- if yongjin is not None:
- new_yongjin = Decimal(yongjin) if yongjin else None
- if new_yongjin != leixing.yongjin:
- changes['yongjin'] = new_yongjin
-
- shenhezhuangtai = request.data.get('shenhezhuangtai')
- if shenhezhuangtai is not None and int(shenhezhuangtai) != leixing.shenhezhuangtai:
- changes['shenhezhuangtai'] = int(shenhezhuangtai)
-
- is_cross = request.data.get('is_cross_enabled')
- if is_cross is not None:
- new_cross = is_cross in [True, 'true', '1']
- if new_cross != leixing.is_cross_enabled:
- changes['is_cross_enabled'] = new_cross
-
- # 图片处理
- image_file = request.FILES.get('file')
- old_image_path = leixing.tupian_url
- if image_file:
- # 验证并上传新图片
- try:
- from utils.oss_utils import validate_image
- valid, msg = validate_image(image_file)
- if not valid:
- return Response({'code': 400, 'msg': msg})
- except ImportError:
- pass
- ext = image_file.name.split('.')[-1].lower()
- new_filename = f"{uuid.uuid4().hex}.{ext}"
- oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}"
- url = upload_to_oss(image_file, oss_path)
- if not url:
- return Response({'code': 500, 'msg': '图片上传失败'})
- changes['tupian_url'] = oss_path
-
- if not changes and not image_file:
- return Response({'code': 400, 'msg': '未做任何修改'})
-
- # 执行更新
- sync_to_products = request.data.get('sync_to_products') in [True, 'true', '1']
- try:
- with transaction.atomic():
- # 更新类型
- for field, value in changes.items():
- setattr(leixing, field, value)
- leixing.save()
-
- # 如果修改了抢单要求相关字段且需要同步商品
- if sync_to_products and ('yaoqiuleixing' in changes or 'huiyuan_id' in changes or 'yongjin' in changes):
- # 更新所有关联此类型的商品
- product_updates = {}
- if 'yaoqiuleixing' in changes:
- product_updates['yaoqiuleixing'] = changes['yaoqiuleixing']
- if 'huiyuan_id' in changes:
- product_updates['huiyuan_id'] = changes['huiyuan_id']
- if 'yongjin' in changes:
- product_updates['yongjin'] = changes['yongjin']
- Shangpin.objects.filter(leixing_id=type_id).update(**product_updates)
-
- # 删除旧图片(新图片上传成功后才删除)
- if image_file and old_image_path:
- delete_from_oss(old_image_path)
-
- return Response({'code': 0, 'msg': '修改成功'})
- except Exception as e:
- logger.error(f"更新类型失败: {e}", exc_info=True)
- # 如果上传了新图片,尝试删除
- if image_file and 'oss_path' in locals():
- delete_from_oss(oss_path)
- return Response({'code': 500, 'msg': '修改失败'})
-
- def delete_leixing(self, request):
- type_id = request.data.get('id')
- if not type_id:
- return Response({'code': 400, 'msg': '缺少类型ID'})
- delete_products = request.data.get('delete_products') in [True, 'true', '1']
-
- try:
- leixing = ShangpinLeixing.objects.get(id=type_id)
- except ShangpinLeixing.DoesNotExist:
- return Response({'code': 404, 'msg': '类型不存在'})
-
- with transaction.atomic():
- # 删除该类型下的所有专区
- zones = ShangpinZhuanqu.objects.filter(leixing_id=type_id)
- if delete_products:
- # 删除专区下的所有商品
- for zone in zones:
- Shangpin.objects.filter(zhuanqu_id=zone.id).delete()
- # 删除所有专区
- zones.delete()
- # 删除该类型下的所有商品(可能还有直接关联类型但无专区的商品)
- Shangpin.objects.filter(leixing_id=type_id).delete()
- else:
- # 只删除专区,不删除商品
- zones.delete()
- # 商品保持原样(leixing_id 保留,但类型已删除,可能导致数据不一致,按需求不处理)
- # 也可以将商品的 leixing_id 置空
- Shangpin.objects.filter(leixing_id=type_id).update(leixing_id=None)
-
- # 删除类型本身
- leixing.delete()
-
- # 删除类型图片
- if leixing.tupian_url:
- delete_from_oss(leixing.tupian_url)
-
- return Response({'code': 0, 'msg': '删除成功'})
-
- # ---------- 商品专区处理 ----------
- def handle_zhuanqu(self, request, action):
- if action == 'add':
- return self.add_zhuanqu(request)
- elif action == 'update':
- return self.update_zhuanqu(request)
- elif action == 'delete':
- return self.delete_zhuanqu(request)
- else:
- return Response({'code': 400, 'msg': '无效的action'})
-
- def add_zhuanqu(self, request):
- mingzi = request.data.get('mingzi', '').strip()
- leixing_id = request.data.get('leixing_id')
- shenhezhuangtai = request.data.get('shenhezhuangtai', 1)
-
- if not mingzi:
- return Response({'code': 400, 'msg': '专区名称不能为空'})
- if not leixing_id:
- return Response({'code': 400, 'msg': '请选择所属类型'})
- # 校验类型是否存在
- if not ShangpinLeixing.objects.filter(id=leixing_id).exists():
- return Response({'code': 404, 'msg': '所属类型不存在'})
-
- try:
- with transaction.atomic():
- zhuanqu = ShangpinZhuanqu.objects.create(
- mingzi=mingzi,
- leixing_id=int(leixing_id),
- shenhezhuangtai=int(shenhezhuangtai),
- paixu=0
- )
- return Response({'code': 0, 'msg': '添加成功', 'data': {'id': zhuanqu.id}})
- except Exception as e:
- logger.error(f"添加专区失败: {e}", exc_info=True)
- return Response({'code': 500, 'msg': '数据库错误'})
-
- def update_zhuanqu(self, request):
- zone_id = request.data.get('id')
- if not zone_id:
- return Response({'code': 400, 'msg': '缺少专区ID'})
- try:
- zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id)
- except ShangpinZhuanqu.DoesNotExist:
- return Response({'code': 404, 'msg': '专区不存在'})
-
- changes = {}
- mingzi = request.data.get('mingzi')
- if mingzi is not None and mingzi != zhuanqu.mingzi:
- changes['mingzi'] = mingzi.strip()
- leixing_id = request.data.get('leixing_id')
- if leixing_id is not None and int(leixing_id) != zhuanqu.leixing_id:
- # 校验新类型存在
- if not ShangpinLeixing.objects.filter(id=leixing_id).exists():
- return Response({'code': 404, 'msg': '新所属类型不存在'})
- changes['leixing_id'] = int(leixing_id)
- shenhezhuangtai = request.data.get('shenhezhuangtai')
- if shenhezhuangtai is not None and int(shenhezhuangtai) != zhuanqu.shenhezhuangtai:
- changes['shenhezhuangtai'] = int(shenhezhuangtai)
-
- if not changes:
- return Response({'code': 400, 'msg': '未做任何修改'})
-
- try:
- with transaction.atomic():
- for field, value in changes.items():
- setattr(zhuanqu, field, value)
- zhuanqu.save()
- return Response({'code': 0, 'msg': '修改成功'})
- except Exception as e:
- logger.error(f"更新专区失败: {e}", exc_info=True)
- return Response({'code': 500, 'msg': '修改失败'})
-
- def delete_zhuanqu(self, request):
- zone_id = request.data.get('id')
- if not zone_id:
- return Response({'code': 400, 'msg': '缺少专区ID'})
- delete_products = request.data.get('delete_products') in [True, 'true', '1']
-
- try:
- zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id)
- except ShangpinZhuanqu.DoesNotExist:
- return Response({'code': 404, 'msg': '专区不存在'})
-
- with transaction.atomic():
- if delete_products:
- Shangpin.objects.filter(zhuanqu_id=zone_id).delete()
- else:
- Shangpin.objects.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None)
- zhuanqu.delete()
-
- return Response({'code': 0, 'msg': '删除成功'})'''
-
-class ModifyProductTypeZoneView(APIView):
- """
- 统一修改商品类型/专区(增、删、改)
- 权限:需要拥有 7007a
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser, MultiPartParser, FormParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- obj_type = request.data.get('type') # 'leixing' 或 'zhuanqu'
- action = request.data.get('action') # 'add', 'update', 'delete'
-
- if not username or not obj_type or not action:
- return Response({'code': 400, 'msg': '缺少必要参数'})
-
- # 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '7007a' not in permissions:
- return Response({'code': 403, 'msg': '您无权进行此操作'})
-
- if obj_type == 'leixing':
- return self.handle_leixing(request, action)
- elif obj_type == 'zhuanqu':
- return self.handle_zhuanqu(request, action)
- else:
- return Response({'code': 400, 'msg': '无效的type参数'})
-
- # ---------- 商品类型处理 ----------
- def handle_leixing(self, request, action):
- if action == 'add':
- return self.add_leixing(request)
- elif action == 'update':
- return self.update_leixing(request)
- elif action == 'delete':
- return self.delete_leixing(request)
- else:
- return Response({'code': 400, 'msg': '无效的action'})
-
- def add_leixing(self, request):
- # 获取并校验字段
- jieshao = request.data.get('jieshao', '').strip()
- if not jieshao:
- return Response({'code': 400, 'msg': '类型名称不能为空'})
-
- # 强制转为整数
- try:
- yaoqiuleixing = int(request.data.get('yaoqiuleixing', 0))
- if yaoqiuleixing not in [1, 2]:
- raise ValueError
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'})
-
- try:
- shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1))
- if shenhezhuangtai not in [1, 2]:
- raise ValueError
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
-
- is_cross_enabled = request.data.get('is_cross_enabled', False)
- is_cross_enabled = is_cross_enabled in [True, 'true', '1', 'on']
-
- huiyuan_id = request.data.get('huiyuan_id', '').strip() or None
- yongjin = request.data.get('yongjin')
- if yongjin is not None and yongjin != '':
- try:
- yongjin = Decimal(str(yongjin))
- except:
- return Response({'code': 400, 'msg': '佣金金额格式错误'})
- else:
- yongjin = None
-
- # 图片校验
- image_file = request.FILES.get('file')
- if not image_file:
- return Response({'code': 400, 'msg': '请上传类型图片'})
-
- # 验证图片
- try:
- from utils.oss_utils import validate_image
- valid, msg = validate_image(image_file)
- if not valid:
- return Response({'code': 400, 'msg': msg})
- except ImportError:
- pass
-
- # 上传图片
- ext = image_file.name.split('.')[-1].lower()
- new_filename = f"{uuid.uuid4().hex}.{ext}"
- oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}"
- url = upload_to_oss(image_file, oss_path)
- if not url:
- return Response({'code': 500, 'msg': '图片上传失败'})
-
- # 创建类型
- try:
- with transaction.atomic():
- leixing = ShangpinLeixing.objects.create(
- jieshao=jieshao,
- tupian_url=oss_path,
- yaoqiuleixing=yaoqiuleixing,
- huiyuan_id=huiyuan_id,
- yongjin=yongjin,
- shenhezhuangtai=shenhezhuangtai,
- is_cross_enabled=is_cross_enabled,
- paixu=0
- )
- return Response({'code': 0, 'msg': '添加成功', 'data': {'id': leixing.id}})
- except Exception as e:
- logger.error(f"添加类型失败: {e}", exc_info=True)
- delete_from_oss(oss_path)
- return Response({'code': 500, 'msg': '数据库错误'})
-
- def update_leixing(self, request):
- type_id = request.data.get('id')
- if not type_id:
- return Response({'code': 400, 'msg': '缺少类型ID'})
- try:
- leixing = ShangpinLeixing.objects.get(id=type_id)
- except ShangpinLeixing.DoesNotExist:
- return Response({'code': 404, 'msg': '类型不存在'})
-
- changes = {}
- # 名称
- jieshao = request.data.get('jieshao')
- if jieshao is not None and jieshao.strip() != leixing.jieshao:
- changes['jieshao'] = jieshao.strip()
-
- # 抢单要求类型(强制转整数)
- yaoqiuleixing = request.data.get('yaoqiuleixing')
- if yaoqiuleixing is not None:
- try:
- new_val = int(yaoqiuleixing)
- if new_val not in [1, 2]:
- raise ValueError
- if new_val != leixing.yaoqiuleixing:
- changes['yaoqiuleixing'] = new_val
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'})
-
- # 会员ID
- huiyuan_id = request.data.get('huiyuan_id', '').strip() or None
- if huiyuan_id != leixing.huiyuan_id:
- changes['huiyuan_id'] = huiyuan_id
-
- # 佣金金额
- yongjin = request.data.get('yongjin')
- if yongjin is not None:
- if yongjin == '':
- new_val = None
- else:
- try:
- new_val = Decimal(str(yongjin))
- except:
- return Response({'code': 400, 'msg': '佣金金额格式错误'})
- if new_val != leixing.yongjin:
- changes['yongjin'] = new_val
-
- # 审核状态
- shenhezhuangtai = request.data.get('shenhezhuangtai')
- if shenhezhuangtai is not None:
- try:
- new_val = int(shenhezhuangtai)
- if new_val not in [1, 2]:
- raise ValueError
- if new_val != leixing.shenhezhuangtai:
- changes['shenhezhuangtai'] = new_val
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
-
- # 跨平台互通
- is_cross = request.data.get('is_cross_enabled')
- if is_cross is not None:
- new_cross = is_cross in [True, 'true', '1', 'on']
- if new_cross != leixing.is_cross_enabled:
- changes['is_cross_enabled'] = new_cross
-
- # 图片处理
- image_file = request.FILES.get('file')
- old_image_path = leixing.tupian_url
- if image_file:
- try:
- from utils.oss_utils import validate_image
- valid, msg = validate_image(image_file)
- if not valid:
- return Response({'code': 400, 'msg': msg})
- except ImportError:
- pass
- ext = image_file.name.split('.')[-1].lower()
- new_filename = f"{uuid.uuid4().hex}.{ext}"
- oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}"
- url = upload_to_oss(image_file, oss_path)
- if not url:
- return Response({'code': 500, 'msg': '图片上传失败'})
- changes['tupian_url'] = oss_path
-
- if not changes and not image_file:
- return Response({'code': 400, 'msg': '未做任何修改'})
-
- sync_to_products = request.data.get('sync_to_products') in [True, 'true', '1', 'on']
- try:
- with transaction.atomic():
- for field, value in changes.items():
- setattr(leixing, field, value)
- leixing.save()
-
- # 同步商品
- if sync_to_products and ('yaoqiuleixing' in changes or 'huiyuan_id' in changes or 'yongjin' in changes):
- product_updates = {}
- if 'yaoqiuleixing' in changes:
- product_updates['yaoqiuleixing'] = changes['yaoqiuleixing']
- if 'huiyuan_id' in changes:
- product_updates['huiyuan_id'] = changes['huiyuan_id']
- if 'yongjin' in changes:
- product_updates['yongjin'] = changes['yongjin']
- if product_updates:
- Shangpin.objects.filter(leixing_id=type_id).update(**product_updates)
-
- # 删除旧图片
- if image_file and old_image_path:
- delete_from_oss(old_image_path)
-
- return Response({'code': 0, 'msg': '修改成功'})
- except Exception as e:
- logger.error(f"更新类型失败: {e}", exc_info=True)
- if image_file and 'oss_path' in locals():
- delete_from_oss(oss_path)
- return Response({'code': 500, 'msg': '修改失败'})
-
- def delete_leixing(self, request):
- type_id = request.data.get('id')
- if not type_id:
- return Response({'code': 400, 'msg': '缺少类型ID'})
- delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on']
-
- try:
- leixing = ShangpinLeixing.objects.get(id=type_id)
- except ShangpinLeixing.DoesNotExist:
- return Response({'code': 404, 'msg': '类型不存在'})
-
- with transaction.atomic():
- # 删除该类型下的所有专区
- zones = ShangpinZhuanqu.objects.filter(leixing_id=type_id)
- if delete_products:
- # 删除专区下的所有商品
- for zone in zones:
- Shangpin.objects.filter(zhuanqu_id=zone.id).delete()
- zones.delete()
- # 删除直接关联该类型的商品
- Shangpin.objects.filter(leixing_id=type_id).delete()
- else:
- zones.delete()
- # 仅清除商品的类型关联
- Shangpin.objects.filter(leixing_id=type_id).update(leixing_id=None)
-
- leixing.delete()
-
- if leixing.tupian_url:
- delete_from_oss(leixing.tupian_url)
-
- return Response({'code': 0, 'msg': '删除成功'})
-
- # ---------- 商品专区处理 ----------
- def handle_zhuanqu(self, request, action):
- if action == 'add':
- return self.add_zhuanqu(request)
- elif action == 'update':
- return self.update_zhuanqu(request)
- elif action == 'delete':
- return self.delete_zhuanqu(request)
- else:
- return Response({'code': 400, 'msg': '无效的action'})
-
- def add_zhuanqu(self, request):
- mingzi = request.data.get('mingzi', '').strip()
- leixing_id = request.data.get('leixing_id')
- if not mingzi:
- return Response({'code': 400, 'msg': '专区名称不能为空'})
- if not leixing_id:
- return Response({'code': 400, 'msg': '请选择所属类型'})
-
- try:
- leixing_id = int(leixing_id)
- if not ShangpinLeixing.objects.filter(id=leixing_id).exists():
- return Response({'code': 404, 'msg': '所属类型不存在'})
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '所属类型ID无效'})
-
- try:
- shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1))
- if shenhezhuangtai not in [1, 2]:
- raise ValueError
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
-
- try:
- with transaction.atomic():
- zhuanqu = ShangpinZhuanqu.objects.create(
- mingzi=mingzi,
- leixing_id=leixing_id,
- shenhezhuangtai=shenhezhuangtai,
- paixu=0
- )
- return Response({'code': 0, 'msg': '添加成功', 'data': {'id': zhuanqu.id}})
- except Exception as e:
- logger.error(f"添加专区失败: {e}", exc_info=True)
- return Response({'code': 500, 'msg': '数据库错误'})
-
- def update_zhuanqu(self, request):
- zone_id = request.data.get('id')
- if not zone_id:
- return Response({'code': 400, 'msg': '缺少专区ID'})
- try:
- zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id)
- except ShangpinZhuanqu.DoesNotExist:
- return Response({'code': 404, 'msg': '专区不存在'})
-
- changes = {}
- mingzi = request.data.get('mingzi')
- if mingzi is not None and mingzi.strip() != zhuanqu.mingzi:
- changes['mingzi'] = mingzi.strip()
-
- leixing_id = request.data.get('leixing_id')
- if leixing_id is not None:
- try:
- new_id = int(leixing_id)
- if not ShangpinLeixing.objects.filter(id=new_id).exists():
- return Response({'code': 404, 'msg': '新所属类型不存在'})
- if new_id != zhuanqu.leixing_id:
- changes['leixing_id'] = new_id
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '所属类型ID无效'})
-
- shenhezhuangtai = request.data.get('shenhezhuangtai')
- if shenhezhuangtai is not None:
- try:
- new_val = int(shenhezhuangtai)
- if new_val not in [1, 2]:
- raise ValueError
- if new_val != zhuanqu.shenhezhuangtai:
- changes['shenhezhuangtai'] = new_val
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
-
- if not changes:
- return Response({'code': 400, 'msg': '未做任何修改'})
-
- try:
- with transaction.atomic():
- for field, value in changes.items():
- setattr(zhuanqu, field, value)
- zhuanqu.save()
- return Response({'code': 0, 'msg': '修改成功'})
- except Exception as e:
- logger.error(f"更新专区失败: {e}", exc_info=True)
- return Response({'code': 500, 'msg': '修改失败'})
-
- def delete_zhuanqu(self, request):
- zone_id = request.data.get('id')
- if not zone_id:
- return Response({'code': 400, 'msg': '缺少专区ID'})
- delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on']
-
- try:
- zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id)
- except ShangpinZhuanqu.DoesNotExist:
- return Response({'code': 404, 'msg': '专区不存在'})
-
- with transaction.atomic():
- if delete_products:
- Shangpin.objects.filter(zhuanqu_id=zone_id).delete()
- else:
- Shangpin.objects.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None)
- zhuanqu.delete()
-
- return Response({'code': 0, 'msg': '删除成功'})
-
-
-# 继续在 houtai/views_product.py 中添加
-class GetRateView(APIView):
- """
- 获取打手分成费率(平台订单和商家订单)
- 权限:需要拥有 7007b
- 请求:POST /houtai/hthqfhpz
- 参数:{"username": "客服手机号"}
- 返回:{"code":0, "data": {"platform_rate": xx, "merchant_rate": xx}}
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '7007b' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看费率'})
-
- from dingdan.models import Lilubiao
- try:
- platform_rate_obj = Lilubiao.objects.get(fadanpingtai='1')
- merchant_rate_obj = Lilubiao.objects.get(fadanpingtai='3')
- except Lilubiao.DoesNotExist as e:
- logger.error(f"费率记录缺失: {e}")
- return Response({'code': 500, 'msg': '费率配置缺失'})
-
- return Response({
- 'code': 0,
- 'data': {
- 'platform_rate': float(platform_rate_obj.lilu) if platform_rate_obj.lilu else 0,
- 'merchant_rate': float(merchant_rate_obj.lilu) if merchant_rate_obj.lilu else 0,
- }
- })
-
-
-class ModifyRateView(APIView):
- """
- 修改打手分成费率
- 权限:需要拥有 7007b
- 请求:POST /houtai/htxgfl
- 参数:{"username": "客服手机号", "type": "platform" / "merchant", "value": 费率(百分比数字)}
- 返回:{"code":0, "msg":"修改成功"}
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- rate_type = request.data.get('type')
- value = request.data.get('value')
-
- if not username or not rate_type or value is None:
- return Response({'code': 400, 'msg': '缺少参数'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '7007b' not in permissions:
- return Response({'code': 403, 'msg': '您无权修改费率'})
-
- try:
- rate_value = Decimal(str(value))
- except:
- return Response({'code': 400, 'msg': '费率值格式错误'})
-
-
- if rate_type == 'platform':
- fadanpingtai = '1'
- elif rate_type == 'merchant':
- fadanpingtai = '3'
- else:
- return Response({'code': 400, 'msg': '无效的type参数'})
-
- try:
- with transaction.atomic():
- rate_obj = Lilubiao.objects.select_for_update().get(fadanpingtai=fadanpingtai)
- rate_obj.lilu = rate_value
- rate_obj.save()
- except Lilubiao.DoesNotExist:
- return Response({'code': 404, 'msg': '费率记录不存在'})
- except Exception as e:
- logger.error(f"修改费率失败: {e}", exc_info=True)
- return Response({'code': 500, 'msg': '修改失败'})
-
- return Response({'code': 0, 'msg': '修改成功'})
-
-
-
-
-
-
-# 权限码(根据你要求,使用 '8080a')
-REQUIRED_PERMISSION = '8080a'
-class PopupNoticeListAPIView(APIView):
- """
- 获取所有弹窗配置(页面 + 弹窗 + 图片)
- URL: POST /houtai/hthqtcxx
- 请求参数: { "username": "客服账号" }
- 响应格式: { "code": 0, "data": { "pages": [...] } }
- 注意:所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段区分
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 获取前端传递的 username(用于防越权)
- username_frontend = request.data.get('username')
- # 2. 调用公共权限验证方法,返回 (客服对象, 权限列表)
- kefu, permissions = verify_kefu_permission(request, username_frontend)
- if kefu is None:
- # 身份验证失败,返回业务码 403,但 HTTP 状态码仍是 200
- return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
-
- # 3. 检查是否拥有所需权限码 '8080a'
- if REQUIRED_PERMISSION not in permissions:
- return Response({'code': 403, 'msg': '您没有权限访问弹窗管理功能'})
-
- # 4. 查询所有页面,并预加载关联的弹窗和图片(避免N+1查询)
- pages = PopupPage.objects.all().prefetch_related('popups__images').order_by('id')
-
- # 5. 序列化输出
- serializer = PopupPageSerializer(pages, many=True)
- return Response({'code': 0, 'data': {'pages': serializer.data}})
-
-
-class PopupNoticeModifyAPIView(APIView):
- """
- 统一修改接口:支持页面、弹窗、图片的增删改
- URL: POST /houtai/htxgtcxx
- 请求中必须包含 action 字段,用于区分操作类型。
- 所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段体现。
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # ---------- 1. 权限验证 ----------
- username_frontend = request.data.get('username')
- kefu, permissions = verify_kefu_permission(request, username_frontend)
- if kefu is None:
- return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
- if REQUIRED_PERMISSION not in permissions:
- return Response({'code': 403, 'msg': '您没有权限操作弹窗管理'})
-
- # ---------- 2. 获取 action 并分发 ----------
- action = request.data.get('action')
- if not action:
- return Response({'code': 400, 'msg': '缺少action参数'})
-
- try:
- if action == 'add_page':
- return self.add_page(request)
- elif action == 'update_page':
- return self.update_page(request)
- elif action == 'delete_page':
- return self.delete_page(request)
- elif action == 'add_popup':
- return self.add_popup(request)
- elif action == 'update_popup':
- return self.update_popup(request)
- elif action == 'delete_popup':
- return self.delete_popup(request)
- elif action == 'add_image':
- return self.add_image(request)
- elif action == 'update_image':
- return self.update_image(request)
- elif action == 'delete_image':
- return self.delete_image(request)
- else:
- return Response({'code': 400, 'msg': f'未知的action: {action}'})
- except Exception as e:
- # 捕获未预期的异常,返回服务器错误
- import traceback
- traceback.print_exc()
- return Response({'code': 500, 'msg': f'服务器错误: {str(e)}'})
-
- # ==================== 页面相关操作 ====================
-
- def add_page(self, request):
- """添加新页面"""
- data = request.data
- # 必填字段校验
- if not data.get('page_key') or not data.get('name'):
- return Response({'code': 400, 'msg': '缺少 page_key 或 name'})
- # 唯一性校验
- if PopupPage.objects.filter(page_key=data['page_key']).exists():
- return Response({'code': 400, 'msg': '页面标识已存在'})
-
- page = PopupPage.objects.create(
- page_key=data['page_key'],
- name=data['name'],
- description=data.get('description', ''),
- is_active=data.get('is_active', True),
- ignore_user_mute=data.get('ignore_user_mute', False)
- )
- return Response({'code': 0, 'msg': '添加成功', 'data': {'id': page.id}})
-
- def update_page(self, request):
- """更新页面信息"""
- page_id = request.data.get('id')
- if not page_id:
- return Response({'code': 400, 'msg': '缺少页面id'})
- try:
- page = PopupPage.objects.get(id=page_id)
- except PopupPage.DoesNotExist:
- return Response({'code': 404, 'msg': '页面不存在'})
-
- # 更新允许修改的字段
- page.page_key = request.data.get('page_key', page.page_key)
- page.name = request.data.get('name', page.name)
- page.description = request.data.get('description', page.description)
- page.is_active = request.data.get('is_active', page.is_active)
- page.ignore_user_mute = request.data.get('ignore_user_mute', page.ignore_user_mute)
- page.save()
- return Response({'code': 0, 'msg': '更新成功'})
-
- def delete_page(self, request):
- """删除页面,可级联删除其下所有弹窗和图片"""
- page_id = request.data.get('id')
- cascade = request.data.get('cascade_delete_popups', True) # 默认级联
- if not page_id:
- return Response({'code': 400, 'msg': '缺少页面id'})
- try:
- page = PopupPage.objects.get(id=page_id)
- except PopupPage.DoesNotExist:
- return Response({'code': 404, 'msg': '页面不存在'})
-
- if cascade:
- # 级联删除:使用事务保证原子性
- with transaction.atomic():
- popups = page.popups.all()
- for popup in popups:
- # 删除弹窗下的所有图片文件
- for img in popup.images.all():
- if img.image_url:
- delete_from_oss(img.image_url)
- popups.delete()
- page.delete()
- else:
- # 非级联删除:如果页面下还有弹窗,则拒绝删除
- if page.popups.exists():
- return Response({'code': 400, 'msg': '该页面下存在弹窗,请先删除弹窗或使用级联删除'})
- page.delete()
-
- return Response({'code': 0, 'msg': '删除成功'})
-
- # ==================== 弹窗相关操作 ====================
-
- def add_popup(self, request):
- """为指定页面添加新弹窗"""
- data = request.data
- page_id = data.get('page_id')
- popup_id = data.get('popup_id')
- if not page_id or not popup_id:
- return Response({'code': 400, 'msg': '缺少 page_id 或 popup_id'})
- try:
- page = PopupPage.objects.get(id=page_id)
- except PopupPage.DoesNotExist:
- return Response({'code': 404, 'msg': '页面不存在'})
-
- # 同一页面下 popup_id 必须唯一
- if PopupConfig.objects.filter(page=page, popup_id=popup_id).exists():
- return Response({'code': 400, 'msg': '该页面下已存在相同的弹窗ID'})
-
- popup = PopupConfig.objects.create(
- page=page,
- popup_id=popup_id,
- title=data.get('title', ''),
- description=data.get('description', ''),
- strategy_type=data.get('strategy_type', 'daily'),
- max_count=data.get('max_count', 1),
- reset_interval=data.get('reset_interval', 'day'),
- duration=data.get('duration', 0),
- force_even_muted=data.get('force_even_muted', False),
- sort_order=data.get('sort_order', 0),
- is_active=data.get('is_active', True),
- start_time=data.get('start_time') or None,
- end_time=data.get('end_time') or None
- )
- return Response({'code': 0, 'msg': '添加成功', 'data': {'id': popup.id}})
-
- def update_popup(self, request):
- """更新弹窗信息"""
- popup_id = request.data.get('id')
- if not popup_id:
- return Response({'code': 400, 'msg': '缺少弹窗id'})
- try:
- popup = PopupConfig.objects.get(id=popup_id)
- except PopupConfig.DoesNotExist:
- return Response({'code': 404, 'msg': '弹窗不存在'})
-
- # 更新弹窗字段
- popup.popup_id = request.data.get('popup_id', popup.popup_id)
- popup.title = request.data.get('title', popup.title)
- popup.description = request.data.get('description', popup.description)
- popup.strategy_type = request.data.get('strategy_type', popup.strategy_type)
- popup.max_count = request.data.get('max_count', popup.max_count)
- popup.reset_interval = request.data.get('reset_interval', popup.reset_interval)
- popup.duration = request.data.get('duration', popup.duration)
- popup.force_even_muted = request.data.get('force_even_muted', popup.force_even_muted)
- popup.sort_order = request.data.get('sort_order', popup.sort_order)
- popup.is_active = request.data.get('is_active', popup.is_active)
- popup.start_time = request.data.get('start_time') or None
- popup.end_time = request.data.get('end_time') or None
- popup.save()
- return Response({'code': 0, 'msg': '更新成功'})
-
- def delete_popup(self, request):
- """删除弹窗,同时删除其下所有图片(包括OSS文件)"""
- popup_id = request.data.get('id')
- if not popup_id:
- return Response({'code': 400, 'msg': '缺少弹窗id'})
- try:
- popup = PopupConfig.objects.get(id=popup_id)
- except PopupConfig.DoesNotExist:
- return Response({'code': 404, 'msg': '弹窗不存在'})
-
- with transaction.atomic():
- for img in popup.images.all():
- if img.image_url:
- delete_from_oss(img.image_url)
- popup.images.all().delete()
- popup.delete()
- return Response({'code': 0, 'msg': '删除成功'})
-
- # ==================== 图片相关操作 ====================
-
- def _upload_image_file(self, file_obj, page_key):
- """
- 内部方法:将图片上传到OSS指定目录
- 目录: a_long/tanchuang/tanchuang/
- 文件名: {page_key}_{时间戳}_{uuid8}.{ext}
- 返回: 相对路径(例如 a_long/tanchuang/tanchuang/dashouzhongxin_20250419120000_abc12345.jpg)
- """
- # 验证图片格式和大小
- valid, msg = validate_image(file_obj)
- if not valid:
- raise ValueError(msg)
-
- # 生成唯一文件名
- ext = os.path.splitext(file_obj.name)[1].lower()
- timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
- unique_id = str(uuid.uuid4())[:8]
- filename = f"{page_key}_{timestamp}_{unique_id}{ext}"
- relative_path = f"a_long/tanchuang/tanchuang/{filename}"
-
- # 上传到OSS
- url = upload_to_oss(file_obj, relative_path)
- if not url:
- raise ValueError("图片上传失败")
- return relative_path
-
- def add_image(self, request):
- """为指定弹窗添加图片(支持上传文件)"""
- popup_id = request.data.get('popup_id')
- if not popup_id:
- return Response({'code': 400, 'msg': '缺少 popup_id'})
- try:
- popup = PopupConfig.objects.get(id=popup_id)
- except PopupConfig.DoesNotExist:
- return Response({'code': 404, 'msg': '弹窗不存在'})
-
- # 获取上传的文件
- file_obj = request.FILES.get('file')
- if not file_obj:
- return Response({'code': 400, 'msg': '未提供图片文件'})
-
- try:
- page_key = popup.page.page_key
- relative_path = self._upload_image_file(file_obj, page_key)
- except ValueError as e:
- return Response({'code': 400, 'msg': str(e)})
-
- # 保存图片记录
- image = PopupImage.objects.create(
- popup_config=popup,
- image_url=relative_path,
- text=request.data.get('text', ''),
- sort_order=request.data.get('sort_order', 0)
- )
- return Response({'code': 0, 'msg': '添加成功', 'data': {'id': image.id}})
-
- def update_image(self, request):
- """更新图片信息(可替换图片文件、修改文字、排序)"""
- image_id = request.data.get('id')
- if not image_id:
- return Response({'code': 400, 'msg': '缺少图片id'})
- try:
- image = PopupImage.objects.get(id=image_id)
- except PopupImage.DoesNotExist:
- return Response({'code': 404, 'msg': '图片不存在'})
-
- # 更新文字和排序
- image.text = request.data.get('text', image.text)
- image.sort_order = request.data.get('sort_order', image.sort_order)
-
- # 如果提供了新图片文件,则替换
- file_obj = request.FILES.get('file')
- if file_obj:
- try:
- page_key = image.popup_config.page.page_key
- new_path = self._upload_image_file(file_obj, page_key)
- except ValueError as e:
- return Response({'code': 400, 'msg': str(e)})
- # 删除旧图片
- if image.image_url:
- delete_from_oss(image.image_url)
- image.image_url = new_path
-
- image.save()
- return Response({'code': 0, 'msg': '更新成功'})
-
- def delete_image(self, request):
- """删除图片(同时删除OSS文件)"""
- image_id = request.data.get('id')
- if not image_id:
- return Response({'code': 400, 'msg': '缺少图片id'})
- try:
- image = PopupImage.objects.get(id=image_id)
- except PopupImage.DoesNotExist:
- return Response({'code': 404, 'msg': '图片不存在'})
-
- if image.image_url:
- delete_from_oss(image.image_url)
- image.delete()
- return Response({'code': 0, 'msg': '删除成功'})
-
-
-
-
-from shangdian.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng
-class ShopListView(APIView):
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 调用公共权限验证(与您给的示例完全一致)
- username_frontend = request.data.get('username')
- kefu, permissions = verify_kefu_permission(request, username_frontend)
- if kefu is None:
- return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
-
- # 2. 检查是否拥有所需权限(999a~999e 任意一个)
- required_perms = {'999a', '999b', '999c', '999d', '999e'}
- if not required_perms.intersection(set(permissions)):
- return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'})
-
- # 3. 解析请求参数
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 10))
- dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip()
- zhuangtai = request.data.get('zhuangtai')
- lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip()
- user_id = request.data.get('user_id', '').strip()
- ketixian_yue_min = request.data.get('ketixian_yue_min')
- ketixian_yue_max = request.data.get('ketixian_yue_max')
- zhengqu_zonge_min = request.data.get('zhengqu_zonge_min')
- zhengqu_zonge_max = request.data.get('zhengqu_zonge_max')
-
- # 4. 构建查询集(先查店铺,预加载关联凭证)
- queryset = Dianpu.objects.select_related('pingzheng').all()
-
- if dianpu_mingcheng:
- queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng)
- if zhuangtai is not None:
- queryset = queryset.filter(zhuangtai=zhuangtai)
- if lianxi_dianhua:
- queryset = queryset.filter(lianxi_dianhua__icontains=lianxi_dianhua)
- if user_id:
- queryset = queryset.filter(pingzheng__yonghu=user_id)
-
- # 金额范围过滤
- if ketixian_yue_min is not None:
- queryset = queryset.filter(ketixian_yue__gte=ketixian_yue_min)
- if ketixian_yue_max is not None:
- queryset = queryset.filter(ketixian_yue__lte=ketixian_yue_max)
- if zhengqu_zonge_min is not None:
- queryset = queryset.filter(zhengqu_zonge__gte=zhengqu_zonge_min)
- if zhengqu_zonge_max is not None:
- queryset = queryset.filter(zhengqu_zonge__lte=zhengqu_zonge_max)
-
- # 5. 分页
- paginator = Paginator(queryset, page_size)
- page_obj = paginator.get_page(page)
-
- # 6. 构造返回数据
- shop_list = []
- for dianpu in page_obj:
- pingzheng = dianpu.pingzheng
- shop_list.append({
- 'id': dianpu.id,
- 'dianpu_mingcheng': dianpu.dianpu_mingcheng,
- 'dianpu_touxiang': dianpu.dianpu_touxiang or '',
- 'lianxi_dianhua': dianpu.lianxi_dianhua or '',
- 'weixinhao': dianpu.weixinhao or '',
- 'zhuangtai': dianpu.zhuangtai,
- 'bangding_yonghushu': dianpu.bangding_yonghushu,
- 'erweima_url': dianpu.erweima_url or '',
- 'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng,
- 'shouyi_choucheng_feilv': dianpu.shouyi_choucheng_feilv or 0,
- 'ketixian_yue': dianpu.ketixian_yue,
- 'zhengqu_zonge': dianpu.zhengqu_zonge,
- 'meiri_tixian_xiane': dianpu.meiri_tixian_xiane or 0,
- 'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane,
- 'pingzheng__yonghu': pingzheng.yonghu, # 用户ID
- 'pingzheng__zhanghao': pingzheng.zhanghao, # 登录账号
- })
-
- # 获取默认配置(ID=1,不存在则创建)
- default_config, _ = DianpuMorenPeizhi.objects.get_or_create(id=1, defaults={
- 'moren_choucheng_feilv': 0.1,
- 'moren_tixian_xiane': 1000.00
- })
-
- return Response({
- 'code': 0,
- 'msg': 'success',
- 'data': {
- 'list': shop_list,
- 'total': paginator.count,
- 'default_config': {
- 'moren_choucheng_feilv': default_config.moren_choucheng_feilv,
- 'moren_tixian_xiane': default_config.moren_tixian_xiane,
- }
- }
- })
-
-from django.db import transaction, IntegrityError
-class ShopModifyView(APIView):
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 身份验证
- username_frontend = request.data.get('username')
- kefu, permissions = verify_kefu_permission(request, username_frontend)
- if kefu is None:
- return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
-
- # 转换权限为集合方便判断
- user_perms = set(permissions)
- action = request.data.get('action', '')
-
- # 2. 根据 action 分派任务
- if action == 'update_dianpu':
- return self._update_dianpu(request, user_perms)
- elif action == 'add_dianpu':
- return self._add_dianpu(request, user_perms)
- elif action == 'update_default':
- return self._update_default(request, user_perms)
- else:
- return Response({'code': 400, 'msg': '未知操作类型'})
-
- def _update_dianpu(self, request, user_perms):
- """修改店铺信息,精确权限控制"""
- dianpu_id = request.data.get('dianpu_id')
- if not dianpu_id:
- return Response({'code': 400, 'msg': '缺少店铺ID'})
-
- try:
- with transaction.atomic():
- dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id)
-
- # 基础信息修改 (999e)
- if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']):
- if '999e' not in user_perms:
- return Response({'code': 403, 'msg': '无权限修改店铺基本信息(999e)'})
- if 'dianpu_mingcheng' in request.data:
- dianpu.dianpu_mingcheng = request.data['dianpu_mingcheng']
- if 'lianxi_dianhua' in request.data:
- dianpu.lianxi_dianhua = request.data['lianxi_dianhua']
- if 'weixinhao' in request.data:
- dianpu.weixinhao = request.data['weixinhao']
-
- # 店铺状态 (999a)
- if 'zhuangtai' in request.data:
- if '999a' not in user_perms:
- return Response({'code': 403, 'msg': '无权限修改店铺状态(999a)'})
- new_status = int(request.data['zhuangtai'])
- if new_status not in (0, 1):
- return Response({'code': 400, 'msg': '店铺状态无效'})
- dianpu.zhuangtai = new_status
-
- # 可提现余额 (999b)
- if 'ketixian_yue' in request.data:
- if '999b' not in user_perms:
- return Response({'code': 403, 'msg': '无权限修改可提现余额(999b)'})
- dianpu.ketixian_yue = request.data['ketixian_yue']
-
- # 提现限额相关 (999c)
- if 'kaiqi_meiri_xiane' in request.data or 'meiri_tixian_xiane' in request.data:
- if '999c' not in user_perms:
- return Response({'code': 403, 'msg': '无权限修改提现限额配置(999c)'})
- if 'kaiqi_meiri_xiane' in request.data:
- dianpu.kaiqi_meiri_xiane = request.data['kaiqi_meiri_xiane']
- if 'meiri_tixian_xiane' in request.data:
- dianpu.meiri_tixian_xiane = request.data['meiri_tixian_xiane']
-
- # 抽成相关 (999d)
- if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data:
- if '999d' not in user_perms:
- return Response({'code': 403, 'msg': '无权限修改抽成配置(999d)'})
- if 'kaiqi_shouyi_choucheng' in request.data:
- dianpu.kaiqi_shouyi_choucheng = request.data['kaiqi_shouyi_choucheng']
- if 'shouyi_choucheng_feilv' in request.data:
- dianpu.shouyi_choucheng_feilv = request.data['shouyi_choucheng_feilv']
-
- dianpu.save() # Django 会自动判断更新哪些字段
- return Response({'code': 0, 'msg': '店铺信息更新成功'})
-
- except Dianpu.DoesNotExist:
- return Response({'code': 404, 'msg': '店铺不存在'})
- except Exception as e:
- logger.exception("修改店铺失败")
- return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
-
- def _add_dianpu(self, request, user_perms):
- """添加店铺,需要权限 999b"""
- if '999b' not in user_perms:
- return Response({'code': 403, 'msg': '无权限添加店铺(999b)'})
-
- yonghu_id = request.data.get('yonghu_id')
- zhanghao = request.data.get('zhanghao')
- mima = request.data.get('mima')
- dianpu_mingcheng = request.data.get('dianpu_mingcheng')
-
- if not all([yonghu_id, zhanghao, mima, dianpu_mingcheng]):
- return Response({'code': 400, 'msg': '必填字段缺失(用户ID、账号、密码、店铺名称)'})
-
- # 检查用户是否存在
- if not UserMain.objects.filter(yonghuid=yonghu_id).exists():
- return Response({'code': 404, 'msg': '用户不存在'})
-
- # 检查凭证是否已存在
- if YonghuPingzheng.objects.filter(zhanghao=zhanghao).exists():
- return Response({'code': 400, 'msg': '登录账号已存在'})
- if YonghuPingzheng.objects.filter(yonghu=yonghu_id).exists():
- return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'})
-
- try:
- with transaction.atomic():
- pingzheng = YonghuPingzheng.objects.create(
- yonghu=yonghu_id,
- zhanghao=zhanghao,
- mima=mima,
- is_active=True
- )
- dianpu = Dianpu.objects.create(
- pingzheng=pingzheng,
- dianpu_mingcheng=dianpu_mingcheng,
- lianxi_dianhua=request.data.get('lianxi_dianhua', ''),
- weixinhao=request.data.get('weixinhao', ''),
- kaiqi_shouyi_choucheng=request.data.get('kaiqi_shouyi_choucheng', False),
- shouyi_choucheng_feilv=request.data.get('shouyi_choucheng_feilv', 0),
- kaiqi_meiri_xiane=request.data.get('kaiqi_meiri_xiane', False),
- meiri_tixian_xiane=request.data.get('meiri_tixian_xiane', 0),
- )
- return Response({'code': 0, 'msg': '店铺创建成功', 'dianpu_id': dianpu.id})
- except IntegrityError as e:
- logger.error("创建店铺违反唯一性约束: %s", e)
- return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'})
- except Exception as e:
- logger.exception("创建店铺失败")
- return Response({'code': 500, 'msg': '创建失败,请稍后重试'})
-
- def _update_default(self, request, user_perms):
- """修改默认配置,按修改内容校验权限"""
- feilv = request.data.get('moren_choucheng_feilv')
- xiane = request.data.get('moren_tixian_xiane')
- modify_commission = feilv is not None
- modify_withdraw = xiane is not None
-
- if modify_commission and '999d' not in user_perms:
- return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'})
- if modify_withdraw and '999c' not in user_perms:
- return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'})
-
- try:
- config, _ = DianpuMorenPeizhi.objects.get_or_create(id=1)
- if modify_commission:
- config.moren_choucheng_feilv = feilv
- if modify_withdraw:
- config.moren_tixian_xiane = xiane
- config.save(update_fields=(['moren_choucheng_feilv'] if modify_commission else []) +
- (['moren_tixian_xiane'] if modify_withdraw else []))
- return Response({'code': 0, 'msg': '默认配置更新成功'})
- except Exception as e:
- logger.exception("更新默认配置失败")
- return Response({'code': 500, 'msg': '更新失败'})
-
-
-
-
-
-# houtai/views.py (完整新增接口,可直接追加到文件末尾)
-import logging
-from django.db import models # Q 查询必须导入
-from django.core.paginator import Paginator
-from django.db import transaction
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from shangpin.models import ShangpinLeixing # 公共商品类型
-from shangdian.models import (
- Dianpu, ShangpinLeixingDianpu,
- DianpuShangpinShenheShezhi, YonghuPingzheng
-)
-from .utils import verify_kefu_permission # 已提供的公共身份校验方法
-
-logger = logging.getLogger(__name__)
-
-# 店铺商品管理所需权限组(拥有任意一个即可访问列表类接口)
-SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
-
-
-# ==================== 1. 公共商品类型 + 审核模式 ====================
-class ShopPublicTypeAndAuditView(APIView):
- """
- 返回:①正常状态的公共商品类型列表 ②全局商品审核模式开关
- 前端请求路径:/houtai/htdphqsplx
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # ---------- 身份与权限校验 ----------
- username = request.data.get('username')
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
-
- # 拥有 1199ab/1199abc/1199abd 任一个即可访问
- if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
- return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'})
-
- # ---------- 查询公共商品类型(仅正常审核状态的) ----------
- try:
- # shenhezhuangtai = 1 表示正常 (根据模型默认值)
- types_qs = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values(
- 'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', 'is_cross_enabled'
- )
- type_list = []
- for t in types_qs:
- type_list.append({
- 'id': t['id'],
- 'jieshao': t['jieshao'] or '',
- 'tupian_url': t['tupian_url'] or '',
- 'yaoqiuleixing': t['yaoqiuleixing'],
- 'huiyuan_id': t['huiyuan_id'],
- 'yongjin': t['yongjin'],
- 'is_cross_enabled': t['is_cross_enabled'],
- })
- except Exception as e:
- logger.exception("查询公共商品类型失败")
- return Response({'code': 500, 'msg': '服务器错误,请稍后重试'})
-
- # ---------- 获取审核模式配置 ----------
- audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first()
- kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False
-
- return Response({
- 'code': 0,
- 'data': {
- 'public_types': type_list,
- 'kaiqi_shenhe': kaiqi_shenhe
- }
- })
-
-
-# ==================== 2. 店铺列表(支持搜索、筛选、分页) ====================
-class ShopListForProductView(APIView):
- """
- 返回店铺列表,用于商品管理页面选择店铺。
- 支持:关键词搜索(店铺ID/名称/用户ID)、店铺状态筛选、分页。
- 前端请求路径:/houtai/htdphqdpys
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- username = request.data.get('username')
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return Response({'code': 403, 'msg': '身份验证失败'})
- if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
- return Response({'code': 403, 'msg': '无权访问店铺列表'})
-
- # ---------- 安全解析分页参数 ----------
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 10))
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '分页参数格式错误'})
-
- # 搜索关键词(字符串,ORM 自动防注入)
- keyword = str(request.data.get('keyword', '')).strip()
- # 店铺状态筛选(1正常,0封禁,不传则全部)
- zhuangtai = request.data.get('zhuangtai')
-
- # ---------- 构建查询集(关联凭证表获取用户ID) ----------
- shops = Dianpu.objects.select_related('pingzheng').all()
-
- # 状态筛选:只接受 0 或 1
- if zhuangtai is not None:
- try:
- zhuangtai = int(zhuangtai)
- if zhuangtai in [0, 1]:
- shops = shops.filter(zhuangtai=zhuangtai)
- else:
- return Response({'code': 400, 'msg': '状态参数只能为0或1'})
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '状态参数必须为整数'})
-
- # 关键词搜索(店铺ID/名称/用户ID)
- if keyword:
- shops = shops.filter(
- models.Q(id__icontains=keyword) |
- models.Q(dianpu_mingcheng__icontains=keyword) |
- models.Q(pingzheng__yonghu__icontains=keyword)
- )
-
- # ---------- 分页 ----------
- paginator = Paginator(shops, page_size)
- page_obj = paginator.get_page(page)
-
- shop_list = []
- for shop in page_obj:
- shop_list.append({
- 'id': shop.id,
- 'dianpu_mingcheng': shop.dianpu_mingcheng,
- 'zhuangtai': shop.zhuangtai,
- 'yonghu_id': shop.pingzheng.yonghu if shop.pingzheng else '',
- 'lianxi_dianhua': shop.lianxi_dianhua or '',
- 'weixinhao': shop.weixinhao or '',
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': shop_list,
- 'total': paginator.count
- }
- })
-
-
-# ==================== 3. 店铺商品类型映射列表(含筛选、待审核计数) ====================
-class ShopProductTypeMappingView(APIView):
- """
- 返回店铺商品类型映射(ShangpinLeixingDianpu)列表。
- 支持:店铺ID、公共类型ID、上架/封禁/审核状态筛选,关键字搜索(介绍/ID/店铺名/公共类型名)。
- 每次请求同时返回“待审核总数”(shenhe_zhuangtai=False 的记录数)。
- 前端请求路径:/houtai/htdphqyssplx
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- username = request.data.get('username')
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return Response({'code': 403, 'msg': '身份验证失败'})
- if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
- return Response({'code': 403, 'msg': '无权访问商品类型映射'})
-
- # ---------- 安全解析参数 ----------
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 10))
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '分页参数格式错误'})
-
- keyword = str(request.data.get('keyword', '')).strip()
- shop_id = request.data.get('shop_id')
- public_type_id = request.data.get('public_type_id')
-
- # 布尔型筛选字段处理:None 表示不筛选,否则转为布尔值
- def parse_bool(val):
- if val is None:
- return None
- return bool(val)
-
- shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
- fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
- shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
-
- # ---------- 查询(连表店铺和公共类型) ----------
- mappings = ShangpinLeixingDianpu.objects.select_related('dianpu', 'gonggong_leixing').all()
-
- # 筛选条件:均为参数化,完全防注入
- if shop_id is not None:
- mappings = mappings.filter(dianpu_id=int(shop_id))
- if public_type_id is not None:
- mappings = mappings.filter(gonggong_leixing_id=int(public_type_id))
- if shangjia is not None:
- mappings = mappings.filter(shangjia_zhuangtai=shangjia)
- if fengjin is not None:
- mappings = mappings.filter(fengjin_zhuangtai=fengjin)
- if shenhe is not None:
- mappings = mappings.filter(shenhe_zhuangtai=shenhe)
-
- # 关键字搜索(介绍、ID、店铺名、公共类型名)
- if keyword:
- mappings = mappings.filter(
- models.Q(jieshao__icontains=keyword) |
- models.Q(id__icontains=keyword) |
- models.Q(dianpu__dianpu_mingcheng__icontains=keyword) |
- models.Q(gonggong_leixing__jieshao__icontains=keyword)
- )
-
- # ---------- 待审核总数(不受分页影响) ----------
- pending_total = mappings.filter(shenhe_zhuangtai=False).count()
-
- # ---------- 分页 ----------
- paginator = Paginator(mappings, page_size)
- page_obj = paginator.get_page(page)
-
- mapping_list = []
- for m in page_obj:
- mapping_list.append({
- 'id': m.id,
- 'dianpu_id': m.dianpu_id,
- 'dianpu_name': m.dianpu.dianpu_mingcheng if m.dianpu else '',
- 'gonggong_leixing_id': m.gonggong_leixing_id,
- 'gonggong_leixing_name': m.gonggong_leixing.jieshao if m.gonggong_leixing else '',
- 'jieshao': m.jieshao,
- 'tupian_url': m.tupian_url or '',
- 'paixu': m.paixu,
- 'shangjia_zhuangtai': m.shangjia_zhuangtai,
- 'fengjin_zhuangtai': m.fengjin_zhuangtai,
- 'shenhe_zhuangtai': m.shenhe_zhuangtai,
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': mapping_list,
- 'total': paginator.count,
- 'pending_total': pending_total
- }
- })
-
-
-# ==================== 统一修改接口(已修正事务) ====================
-class ShopProductModifyView(APIView):
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- username = request.data.get('username')
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return Response({'code': 403, 'msg': '身份验证失败'})
-
- user_perms = set(permissions)
- action = request.data.get('action')
-
- if action == 'update_audit_mode':
- return self._update_audit_mode(request, user_perms)
- elif action == 'update_mapping_status':
- return self._update_mapping_status(request, user_perms)
- else:
- return Response({'code': 400, 'msg': '未知操作'})
-
- @transaction.atomic # ✅ 事务修复点
- def _update_audit_mode(self, request, user_perms):
- if '1199ab' not in user_perms:
- return Response({'code': 403, 'msg': '无权限修改审核模式'})
-
- kaiqi = request.data.get('kaiqi_shenhe')
- if kaiqi is None:
- return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'})
-
- try:
- config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1)
- config.kaiqi_shenhe = bool(kaiqi)
- config.save(update_fields=['kaiqi_shenhe'])
- return Response({'code': 0, 'msg': '审核模式已更新'})
- except Exception as e:
- logger.exception("更新审核模式失败")
- return Response({'code': 500, 'msg': '服务器错误'})
-
- @transaction.atomic
- def _update_mapping_status(self, request, user_perms):
- mapping_id = request.data.get('mapping_id')
- if not mapping_id:
- return Response({'code': 400, 'msg': '缺少 mapping_id'})
-
- try:
- mapping = ShangpinLeixingDianpu.objects.select_for_update().get(id=int(mapping_id))
- except (ShangpinLeixingDianpu.DoesNotExist, ValueError):
- return Response({'code': 404, 'msg': '映射不存在'})
-
- fengjin = request.data.get('fengjin_zhuangtai')
- shangjia = request.data.get('shangjia_zhuangtai')
- shenhe = request.data.get('shenhe_zhuangtai')
-
- # 权限判断
- if fengjin is not None:
- if fengjin:
- if '1199abc' not in user_perms:
- return Response({'code': 403, 'msg': '无权限封禁'})
- else:
- if '1199abd' not in user_perms:
- return Response({'code': 403, 'msg': '无权限解封'})
- mapping.fengjin_zhuangtai = bool(fengjin)
-
- if shangjia is not None:
- if not shangjia:
- if '1199abc' not in user_perms:
- return Response({'code': 403, 'msg': '无权限下架'})
- else:
- if '1199abd' not in user_perms:
- return Response({'code': 403, 'msg': '无权限上架'})
- mapping.shangjia_zhuangtai = bool(shangjia)
-
- if shenhe is not None:
- if shenhe:
- if '1199abd' not in user_perms:
- return Response({'code': 403, 'msg': '无权限审核通过'})
- mapping.shenhe_zhuangtai = bool(shenhe)
-
- mapping.save()
- return Response({'code': 0, 'msg': '修改成功'})
-
-
-
-
-
-
-# 商品管理相关权限组(拥有任意一个即可查看列表)
-PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd', '1199abe']
-
-
-class ProductListView(APIView):
- """
- 商品列表接口,支持多条件筛选与分页。
- 请求路径: /houtai/hqhtdpsplx
- 权限要求: 1199ab / 1199abc / 1199abd / 1199abe 任一即可
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # ----- 身份与权限验证 -----
- username = request.data.get('username')
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return Response({'code': 403, 'msg': '身份验证失败'})
-
- if not set(permissions).intersection(PRODUCT_PERMS):
- return Response({'code': 403, 'msg': '无商品管理权限'})
-
- # ----- 安全解析分页与筛选参数 -----
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 15))
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '分页参数格式错误'})
-
- keyword = str(request.data.get('keyword', '')).strip()
- shop_id = request.data.get('shop_id') # 店铺ID (可选)
- shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型映射ID (可选)
- public_type_id = request.data.get('public_type_id') # 公共商品类型ID (可选)
-
- # 布尔型状态筛选
- def parse_bool(val):
- if val is None:
- return None
- return bool(val)
-
- shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
- shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
- fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
-
- # ----- 构建查询(预加载店铺和店铺商品类型) -----
- products = Shangpin.objects.select_related('dianpu', 'dianpu_leixing').all()
-
- # 按店铺筛选
- if shop_id is not None:
- try:
- products = products.filter(dianpu_id=int(shop_id))
- except (ValueError, TypeError):
- return Response({'code': 400, 'msg': 'shop_id 格式错误'})
-
- # 按店铺商品类型映射筛选
- if shop_product_type_id is not None:
- try:
- products = products.filter(dianpu_leixing_id=int(shop_product_type_id))
- except (ValueError, TypeError):
- return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'})
-
- # 按公共商品类型筛选(leixing_id 存储的是公共类型的ID)
- if public_type_id is not None:
- try:
- products = products.filter(leixing_id=int(public_type_id))
- except (ValueError, TypeError):
- return Response({'code': 400, 'msg': 'public_type_id 格式错误'})
-
- # 平台审核状态筛选 (BooleanField)
- if shenhe is not None:
- products = products.filter(shenhe_zhuangtai=shenhe)
-
- # 上架状态筛选
- if shangjia is not None:
- products = products.filter(shangjia_zhuangtai=shangjia)
-
- # 封禁状态筛选
- if fengjin is not None:
- products = products.filter(fengjin_zhuangtai=fengjin)
-
- # 关键词搜索:商品ID或商品标题(搜索具有模糊性,使用icontains)
- if keyword:
- # 尝试纯数字可能是商品ID,使用精确匹配与标题模糊搜索
- products = products.filter(
- models.Q(id__icontains=keyword) |
- models.Q(biaoqian__icontains=keyword)
- )
-
- # ----- 获取待审核总数(平台审核未通过的数量) -----
- pending_total = products.filter(shenhe_zhuangtai=False).count()
-
- # ----- 分页 -----
- paginator = Paginator(products, page_size)
- page_obj = paginator.get_page(page)
-
- # ----- 构造返回数据(批量获取公共类型名称,避免N+1) -----
- # 提取所有公共商品类型ID
- type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None}
- public_type_map = {}
- if type_ids:
- public_types = ShangpinLeixing.objects.filter(id__in=type_ids).values('id', 'jieshao')
- public_type_map = {t['id']: t['jieshao'] for t in public_types}
-
- product_list = []
- for p in page_obj:
- product_list.append({
- 'id': p.id,
- 'biaoqian': p.biaoqian or '',
- 'jiage': p.jiage,
- 'kucun': p.kucun,
- 'leixing_id': p.leixing_id,
- 'public_type_name': public_type_map.get(p.leixing_id, ''),
- 'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
- 'duiwai_xiaoliang': p.duiwai_xiaoliang,
- 'jieshao': p.jieshao or '',
- 'xiadan_xuzhi': p.xiadan_xuzhi or '',
- 'guize_tupian': p.guize_tupian or '',
- 'yaoqiuleixing': p.yaoqiuleixing,
- 'huiyuan_id': p.huiyuan_id or '',
- 'yongjin': p.yongjin,
- 'kaioi_ewai_dashou_fencheng': p.kaioi_ewai_dashou_fencheng,
- 'ewai_dashou_fencheng': p.ewai_dashou_fencheng,
- 'create_time': p.create_time.isoformat() if p.create_time else '',
- 'update_time': p.update_time.isoformat() if p.update_time else '',
- 'paixu': p.paixu,
- 'dianpu_id': p.dianpu_id,
- 'dianpu_name': p.dianpu.dianpu_mingcheng if p.dianpu else '',
- 'dianpu_leixing_id': p.dianpu_leixing_id,
- 'dianpu_leixing_name': p.dianpu_leixing.jieshao if p.dianpu_leixing else '',
- 'shangjia_zhuangtai': p.shangjia_zhuangtai,
- 'fengjin_zhuangtai': p.fengjin_zhuangtai,
- 'shenhe_zhuangtai': p.shenhe_zhuangtai,
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': product_list,
- 'total': paginator.count,
- 'pending_total': pending_total, # 待审核商品总数
- }
- })
-
-
-class ProductModifyView(APIView):
- """
- 商品修改接口,根据修改内容细粒度鉴权。
- 请求路径: /houtai/htxgdpspsj
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- username = request.data.get('username')
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return Response({'code': 403, 'msg': '身份验证失败'})
-
- user_perms = set(permissions)
- action = request.data.get('action', '')
-
- if action != 'update_product':
- return Response({'code': 400, 'msg': '未知操作'})
-
- return self._update_product(request, user_perms)
-
- @transaction.atomic
- def _update_product(self, request, user_perms):
- """执行商品修改,严格校验权限"""
- product_id = request.data.get('product_id')
- if not product_id:
- return Response({'code': 400, 'msg': '缺少商品ID'})
-
- try:
- # 行级锁防止并发修改
- product = Shangpin.objects.select_for_update().get(id=int(product_id))
- except (Shangpin.DoesNotExist, ValueError):
- return Response({'code': 404, 'msg': '商品不存在'})
-
- # 需要修改的字段
- fields_to_update = {}
- # 将要修改的状态标志用于后续权限判断
- status_changes = {
- 'shangjia': request.data.get('shangjia_zhuangtai'),
- 'fengjin': request.data.get('fengjin_zhuangtai'),
- 'shenhe': request.data.get('shenhe_zhuangtai'),
- }
-
- # ---------- 权限分类检查 ----------
- # 1. 封禁/下架操作 (消极操作) -> 1199abc
- if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']):
- if '1199abc' not in user_perms:
- return Response({'code': 403, 'msg': '无权限执行下架/封禁/驳回审核操作(1199abc)'})
- if status_changes.get('fengjin') is not None and status_changes['fengjin'] == True:
- if '1199abc' not in user_perms:
- return Response({'code': 403, 'msg': '无权限执行封禁操作(1199abc)'})
-
- # 2. 解封/上架/过审 (积极操作) -> 1199abd
- if any(status_changes.get(k) is not None and status_changes[k] == True for k in ['shangjia', 'shenhe']):
- if '1199abd' not in user_perms:
- return Response({'code': 403, 'msg': '无权限执行上架/解封/审核通过操作(1199abd)'})
- if status_changes.get('fengjin') is not None and status_changes['fengjin'] == False:
- if '1199abd' not in user_perms:
- return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'})
-
- # 3. 修改其他数据(非状态字段)-> 1199abe
- # 除了上架、封禁、审核之外的字段都属于“其他数据”
- other_fields = [k for k in request.data.keys() if k not in ['action', 'product_id', 'username',
- 'shangjia_zhuangtai', 'fengjin_zhuangtai',
- 'shenhe_zhuangtai']]
- if other_fields and '1199abe' not in user_perms:
- return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'})
-
- # ---------- 应用修改 ----------
- # 允许修改的字段白名单(防止注入)
- allowed_fields = [
- 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'duiwai_xiaoliang',
- 'jieshao', 'xiadan_xuzhi', 'yaoqiuleixing', 'yongjin',
- 'kaioi_ewai_dashou_fencheng', 'ewai_dashou_fencheng',
- 'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai'
- ]
-
- for field in allowed_fields:
- value = request.data.get(field)
- if value is not None:
- # 简单类型校验(根据字段类型)
- if field in ['jiage', 'kucun', 'duiwai_xiaoliang', 'yongjin', 'ewai_dashou_fencheng', 'paixu']:
- try:
- value = float(value) if 'jiage' in field or 'yongjin' in field or 'ewai' in field else int(value)
- except (ValueError, TypeError):
- return Response({'code': 400, 'msg': f'字段 {field} 值格式错误'})
- setattr(product, field, value)
-
- product.save(update_fields=allowed_fields)
- return Response({'code': 0, 'msg': '商品修改成功'})
-
-
-
-
-# 与前端约定的聊天权限码
-CHAT_PERM_CODES = ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4']
-
-class KefuChatPermissionsView(APIView):
- """
- 获取客服聊天权限及GoEasy配置
- POST /houtai/kfhqltqx
- Body: { "phone": "客服手机号" }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 从前端获取传递的账号,用于防越权校验
- username_from_frontend = request.data.get('phone', None)
-
- # 2. 调用公共验证方法。
- # 返回 (kefu_obj, permissions) 或 (None, Response)
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
-
- # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值(错误Response)
- if kefu_obj is None:
- return permissions
-
- # ---------- 验证通过,permissions 已经是权限码列表 ----------
- # 4. 检查是否拥有至少一个聊天权限
- has_chat_perm = any(p in CHAT_PERM_CODES for p in permissions)
-
- # 5. 若有聊天权限,查询我方俱乐部的 chat_api_key
- goeasy_appkey = ''
- if has_chat_perm:
- config = ClubConfig.objects.filter(is_self=1).first()
- if config:
- goeasy_appkey = config.chat_api_key or ''
-
- # 6. 返回数据
- return Response({
- 'code': 0,
- 'msg': 'ok',
- 'data': {
- 'permissions': permissions,
- 'goeasy_appkey': goeasy_appkey
- }
- })
-
-
-
-
-from rest_framework.permissions import AllowAny
-
-
-
-from dingdan.models import Dingdan, Fadan, CrossPlatformOrderData # 按实际路径
-class FineApplyView(APIView):
- """
- 客服申请罚款接口(金额罚款,非积分)
- 路径:POST /houtai/kffkdssq
- 参数:
- username: 客服手机号
- dingdan_id: 订单ID
- chufaliyou: 罚款原因
- fakuanjine: 罚款金额(元)
- yingxiang_qiangdan: 1影响抢单 / 0不影响抢单
- shenqingren_shenfen: 操作人身份 (1客服/2售后/3管理员等)
- 权限:JWT + 权限码99933abs
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 提取参数
- username = request.data.get('username', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- chufaliyou = request.data.get('chufaliyou', '').strip()
- fakuanjine = request.data.get('fakuanjine', 0)
- yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1)
- shenqingren_shenfen = request.data.get('shenqingren_shenfen', 2)
-
- if not all([username, dingdan_id, chufaliyou, fakuanjine]):
- return Response({'code': 400, 'msg': '参数不完整'})
-
- try:
- # 2. 权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if '99933abs' not in permissions:
- return Response({'code': 403, 'msg': '您无罚款权限'})
-
- # 3. 获取订单(无需 select_for_update,因不修改订单)
- try:
- order = Dingdan.objects.get(dingdan_id=dingdan_id)
- except Dingdan.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'})
-
- dashou_id = order.jiedan_dashou_id
- if not dashou_id:
- return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'})
-
- # 4. 重复罚单检查
- if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists():
- return Response({'code': 400, 'msg': '该打手已被罚款过'})
-
- # 5. 跨平台相关字段
- partner_order_id = order.partner_order_id or ''
- partner_club_id = order.partner_club_id or ''
- dispatch_type = order.dispatch_type or 0
-
- # 本地创建罚单的可复用函数
- def create_local_fadan():
- Fadan.objects.create(
- beichufa_id=dashou_id,
- shenqing_chufa=username,
- shenfen=1, # 1 代表打手
- chufaliyou=chufaliyou,
- fakuanjine=fakuanjine,
- guanliandingdan_id=dingdan_id,
- zhuangtai=1, # 待缴纳
- yingxiang_qiangdan=yingxiang_qiangdan,
- shenqingren_shenfen=shenqingren_shenfen,
- partner_club_id='',
- )
-
- # 6. 判断:没有对方订单ID,或对方派单 → 本地处罚
- if not partner_order_id or dispatch_type == 2:
- with transaction.atomic():
- create_local_fadan()
- logger.info(f"本地罚款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}")
- return Response({'code': 0, 'msg': '罚款已生成'})
-
- # 7. 我方派单且有对方订单ID → 通知对方平台
- # 获取我方俱乐部ID
- try:
- my_club = ClubConfig.objects.filter(is_self=1).first()
- if not my_club:
- return Response({'code': 500, 'msg': '系统配置错误'})
- self_club_id = my_club.club_id
- club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
- partner_domain = club_rel.partner_domain
- except ObjectDoesNotExist:
- return Response({'code': 500, 'msg': '对方平台信息未配置'})
-
- url = partner_domain.rstrip('/') + '/houtai/kffkdssq_notify'
- payload = {
- 'order_id': partner_order_id,
- 'club_id': self_club_id,
- 'chufaliyou': chufaliyou,
- 'fakuanjine': fakuanjine,
- 'yingxiang_qiangdan': yingxiang_qiangdan,
- 'shenqingren_shenfen': shenqingren_shenfen,
- 'operator': username,
- }
- timeout = 3
- retries = 2
- success = False
- for attempt in range(retries):
- try:
- resp = requests.post(url, json=payload, timeout=timeout)
- if resp.status_code == 200 and resp.json().get('code') == 0:
- success = True
- break
- except Exception:
- continue
-
- if not success:
- return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'})
-
- # 通知成功后,本地也记录一笔(标记为跨平台)
- with transaction.atomic():
- Fadan.objects.create(
- beichufa_id=dashou_id,
- shenqing_chufa=username,
- shenfen=1,
- chufaliyou=chufaliyou,
- fakuanjine=fakuanjine,
- guanliandingdan_id=dingdan_id,
- zhuangtai=1,
- yingxiang_qiangdan=yingxiang_qiangdan,
- shenqingren_shenfen=shenqingren_shenfen,
- partner_club_id=partner_club_id,
- )
- logger.info(f"跨平台罚款通知成功: {dingdan_id}")
- return Response({'code': 0, 'msg': '罚款已提交至对方平台'})
-
- except Exception as e:
- logger.error(f"罚款申请异常: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-
-
-
-class PartnerFineNotifyView(APIView):
- """
- 接收对方平台罚款通知(无认证)
- 路径:POST /houtai/kffkdssq_notify
- 参数:
- order_id: 对方平台的订单ID(需通过这个找到我方订单)
- club_id: 对方俱乐部ID
- chufaliyou: 罚款原因
- fakuanjine: 罚款金额
- yingxiang_qiangdan: 1影响抢单 / 0不影响
- shenqingren_shenfen: 申请处罚人身份
- operator: 操作人标识
- """
- authentication_classes = []
- permission_classes = [AllowAny]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # token = request.data.get('token')
- # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN):
- # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200)
- try:
- order_id = request.data.get('order_id', '').strip() # 对方平台订单ID
- club_id = request.data.get('club_id', '').strip()
- chufaliyou = request.data.get('chufaliyou', '').strip()
- fakuanjine = request.data.get('fakuanjine', 0)
- yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1)
- shenqingren_shenfen = request.data.get('shenqingren_shenfen', 2)
- operator = request.data.get('operator', '').strip()
-
- if not all([order_id, chufaliyou]):
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 通过对方订单ID查找我方订单
- try:
- cross = CrossPlatformOrderData.objects.get(partner_order_id=order_id)
- my_dingdan_id = cross.dingdan_id
- order = Dingdan.objects.get(dingdan_id=my_dingdan_id)
- dashou_id = order.jiedan_dashou_id
- except CrossPlatformOrderData.DoesNotExist:
- return Response({'code': 404, 'msg': '未找到关联订单'})
- except Dingdan.DoesNotExist:
- return Response({'code': 404, 'msg': '我方订单不存在'})
-
- if not dashou_id:
- return Response({'code': 400, 'msg': '订单无接单打手'})
-
- # 重复检查
- if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=my_dingdan_id).exists():
- return Response({'code': 0, 'msg': '已存在罚单'})
-
- # 创建罚单
- Fadan.objects.create(
- beichufa_id=dashou_id,
- shenqing_chufa=operator,
- shenfen=1,
- chufaliyou=chufaliyou,
- fakuanjine=fakuanjine,
- guanliandingdan_id=my_dingdan_id,
- zhuangtai=1,
- yingxiang_qiangdan=yingxiang_qiangdan,
- shenqingren_shenfen=shenqingren_shenfen,
- partner_club_id=club_id,
- )
- logger.info(f"接收对方罚款成功: 订单 {my_dingdan_id}, 打手 {dashou_id}")
- return Response({'code': 0, 'msg': 'success'})
-
- except Exception as e:
- logger.error(f"接收罚款通知异常: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-
-
-
-
-
-from dingdan.models import Fadan, FadanShensuTupian
-# 权限码到身份的映射
-PERMISSION_TO_SHENFEN = {
- '66693a': 1, # 打手
- '66693b': 3, # 商家
- '66693c': 2, # 管事
- '66694c': 4, # 组长
-}
-
-# 身份对应的用户扩展关系名
-SHENFEN_PROFILE_MAP = {
- 1: 'dashou_profile',
- 2: 'guanshi_profile',
- 3: 'shop_profile',
- 4: 'zuzhang_profile',
-}
-
-
-def check_fadan_permission(permissions, shenfen):
- """检查是否有处理指定身份罚单的权限"""
- perm_code = {1: '66693a', 2: '66693c', 3: '66693b', 4: '66694c'}.get(shenfen)
- return perm_code in permissions
-
-
-class FaKuanTongJiView(APIView):
- """
- 罚款统计接口
- POST /houtai/htglyhqcfsltj
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- try:
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- # 至少有一个罚款管理权限
- if not any(p in permissions for p in PERMISSION_TO_SHENFEN.keys()):
- return Response({'code': 403, 'msg': '无权限查看罚款统计'})
-
- stats = Fadan.objects.aggregate(
- total=Count('id'),
- daijiaona=Count('id', filter=Q(zhuangtai=1)),
- shensuzhong=Count('id', filter=Q(zhuangtai=3)),
- yijiaona=Count('id', filter=Q(zhuangtai=2)),
- yibohui=Count('id', filter=Q(zhuangtai=4)),
- )
- data = {k: v or 0 for k, v in stats.items()}
- return Response({'code': 0, 'msg': '成功', 'data': data})
- except Exception as e:
- logger.error(f"罚款统计异常: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '系统繁忙'})
-
-
-class FaKuanLieBiaoView(APIView):
- """
- 罚款列表接口
- POST /houtai/hthqfklb
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- try:
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
- if not any(p in permissions for p in PERMISSION_TO_SHENFEN.keys()):
- return Response({'code': 403, 'msg': '无权限查看罚款列表'})
-
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 20))
- if page_size > 100:
- page_size = 100
-
- qs = Fadan.objects.all()
-
- # 状态筛选
- zhuangtai = request.data.get('zhuangtai')
- if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]:
- qs = qs.filter(zhuangtai=int(zhuangtai))
-
- # 被处罚人ID
- beichufa_id = request.data.get('beichufa_id', '').strip()
- if beichufa_id:
- qs = qs.filter(beichufa_id=beichufa_id)
-
- # 被处罚人身份
- beichufa_shenfen = request.data.get('beichufa_shenfen')
- if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]:
- qs = qs.filter(shenfen=int(beichufa_shenfen))
-
- # 模糊搜索(订单ID、被罚人ID、申请人ID)
- sousuo = request.data.get('sousuo', '').strip()
- if sousuo:
- qs = qs.filter(
- Q(guanliandingdan_id__icontains=sousuo) |
- Q(beichufa_id__icontains=sousuo) |
- Q(shenqing_chufa__icontains=sousuo)
- )
-
- qs = qs.order_by('-create_time')
- paginator = Paginator(qs, page_size)
- try:
- page_obj = paginator.page(page)
- except Exception:
- return Response({
- 'code': 0, 'msg': '成功',
- 'data': {'list': [], 'total': 0, 'page': page, 'page_size': page_size}
- })
-
- records = list(page_obj)
- fadan_ids = [r.id for r in records]
-
- # 批量获取图片并分组
- zhengju_map = {}
- shensu_map = {}
- if fadan_ids:
- tupians = FadanShensuTupian.objects.filter(
- fadan_id__in=fadan_ids
- ).values('fadan_id', 'tupian_url', 'yongtu')
- for t in tupians:
- fid = t['fadan_id']
- url = t['tupian_url']
- if t['yongtu'] == 1:
- zhengju_map.setdefault(fid, []).append(url)
- else:
- shensu_map.setdefault(fid, []).append(url)
-
- # 组装列表数据
- results = []
- for r in records:
- results.append({
- 'id': r.id,
- 'beichufa_id': r.beichufa_id,
- 'shenfen': r.shenfen,
- 'shenfen_text': dict(Fadan._meta.get_field('shenfen').choices).get(r.shenfen, ''),
- 'shenqing_chufa': r.shenqing_chufa or '',
- 'shenqingren_shenfen': r.shenqingren_shenfen,
- 'chufaliyou': r.chufaliyou or '',
- 'fakuanjine': str(r.fakuanjine),
- 'guanliandingdan_id': r.guanliandingdan_id or '',
- 'zhuangtai': r.zhuangtai,
- 'yingxiang_qiangdan': r.yingxiang_qiangdan,
- 'shensuliyou': r.shensuliyou or '',
- 'bohuiliyou': r.bohuiliyou or '',
- 'chulizhe': r.chulizhe or '',
- 'chulizhe_shenfen': r.chulizhe_shenfen,
- 'zhengju_tupian': zhengju_map.get(r.id, []),
- 'shensu_tupian': shensu_map.get(r.id, []),
- 'create_time': r.create_time.strftime('%Y-%m-%d %H:%M:%S') if r.create_time else '',
- 'update_time': r.update_time.strftime('%Y-%m-%d %H:%M:%S') if r.update_time else '',
- })
-
- return Response({
- 'code': 0, 'msg': '成功',
- 'data': {
- 'list': results,
- 'total': paginator.count,
- 'page': page,
- 'page_size': page_size,
- }
- })
- except Exception as e:
- logger.error(f"罚款列表异常: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '系统繁忙'})
-
-
-class FaKuanChuLiView(APIView):
- """
- 处理罚款申诉(同意/拒绝)
- POST /houtai/glyclfk
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [MultiPartParser, FormParser, JSONParser]
-
- def post(self, request):
- try:
- username = request.data.get('username', '').strip()
- fadan_id = request.data.get('fadan_id')
- action = request.data.get('action', '').strip()
- chuli_liyou = request.data.get('chuli_liyou', '').strip()
-
- if not all([username, fadan_id, action]):
- return Response({'code': 400, 'msg': '参数不完整'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- with transaction.atomic():
- try:
- fadan = Fadan.objects.select_for_update().get(id=fadan_id)
- except Fadan.DoesNotExist:
- return Response({'code': 404, 'msg': '罚单不存在'})
-
- if fadan.zhuangtai != 3:
- return Response({'code': 400, 'msg': '当前状态不可处理'})
-
- if not check_fadan_permission(permissions, fadan.shenfen):
- return Response({'code': 403, 'msg': '无权限处理该类罚单'})
-
- # 处理上传的图片
- uploaded_urls = []
- files = request.FILES.getlist('tupian') or []
- if not files:
- single = request.FILES.get('tupian')
- if single:
- files = [single]
- for file in files:
- valid, msg = validate_image(file)
- if not valid:
- return Response({'code': 400, 'msg': msg})
- ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
- file_name = f"chufa_{fadan_id}_{int(time.time() * 1000)}.{ext}"
- file_path = f"chufatupian/kechufatupian/{file_name}"
- url = upload_to_oss(file, file_path)
- if not url:
- return Response({'code': 500, 'msg': '图片上传失败'})
- uploaded_urls.append(file_path)
-
- # 更新状态
- if action == 'agree': # 同意申诉 → 驳回处罚
- fadan.zhuangtai = 4
- fadan.bohuiliyou = chuli_liyou
- elif action == 'reject': # 拒绝申诉 → 回到待缴纳
- fadan.zhuangtai = 1
- fadan.bohuiliyou = chuli_liyou
- else:
- return Response({'code': 400, 'msg': '无效的操作类型'})
-
- # 记录处理者(使用 request.user 的 phone)
- fadan.chulizhe = request.user.phone
- fadan.chulizhe_shenfen = 2 # 这里假设处理者是售后身份,可根据实际业务获取
- fadan.save()
-
- # 保存图片(证据图片,用途1)
- for relative_url in uploaded_urls:
- FadanShensuTupian.objects.create(
- fadan_id=fadan_id,
- beichufa_id=fadan.beichufa_id,
- tupian_url=relative_url,
- yongtu=1
- )
-
- logger.info(f"罚款处理成功: {fadan_id}, 操作: {action}")
- return Response({'code': 0, 'msg': '处理成功'})
-
- except Exception as e:
- logger.error(f"罚款处理异常: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '系统繁忙'})
-
-
-class FaKuanChuangJianView(APIView):
- """
- 管理员创建罚款
- POST /houtai/htfksc
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [MultiPartParser, FormParser, JSONParser]
-
- def post(self, request):
- try:
- username = request.data.get('username', '').strip()
- beichufa_id = request.data.get('beichufa_id', '').strip()
- shenfen = request.data.get('shenfen')
- chufaliyou = request.data.get('chufaliyou', '').strip()
- fakuanjine = request.data.get('fakuanjine')
- yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1)
-
- if not all([username, beichufa_id, shenfen, chufaliyou, fakuanjine]):
- return Response({'code': 400, 'msg': '参数不完整'})
-
- try:
- shenfen = int(shenfen)
- fakuanjine = float(fakuanjine)
- except (ValueError, TypeError):
- return Response({'code': 400, 'msg': '参数格式错误'})
-
- if shenfen not in [1, 2, 3, 4]:
- return Response({'code': 400, 'msg': '无效的被罚人身份'})
-
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- if not check_fadan_permission(permissions, shenfen):
- return Response({'code': 403, 'msg': '无权限创建该类罚单'})
-
- # 验证用户是否存在对应的扩展表
- try:
- user = UserMain.objects.get(yonghuid=beichufa_id)
- profile_attr = SHENFEN_PROFILE_MAP.get(shenfen)
- if not profile_attr or not hasattr(user, profile_attr):
- return Response({'code': 400, 'msg': '该用户不是所选身份'})
- except UserMain.DoesNotExist:
- return Response({'code': 404, 'msg': '用户不存在'})
-
- # 处理图片上传
- uploaded_urls = []
- files = request.FILES.getlist('tupian') or []
- if not files:
- single = request.FILES.get('tupian')
- if single:
- files = [single]
- for file in files:
- valid, msg = validate_image(file)
- if not valid:
- return Response({'code': 400, 'msg': msg})
- ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
- file_name = f"chufa_{int(time.time() * 1000)}.{ext}"
- file_path = f"chufatupian/kechufatupian/{file_name}"
- url = upload_to_oss(file, file_path)
- if not url:
- return Response({'code': 500, 'msg': '图片上传失败'})
- uploaded_urls.append(file_path)
-
- with transaction.atomic():
- fadan = Fadan.objects.create(
- beichufa_id=beichufa_id,
- shenfen=shenfen,
- shenqing_chufa=request.user.phone, # 申请人(客服)手机号
- shenqingren_shenfen=2, # 售后身份
- chufaliyou=chufaliyou,
- fakuanjine=fakuanjine,
- yingxiang_qiangdan=yingxiang_qiangdan,
- zhuangtai=1, # 待缴纳
- )
- for relative_url in uploaded_urls:
- FadanShensuTupian.objects.create(
- fadan=fadan,
- beichufa_id=beichufa_id,
- tupian_url=relative_url,
- yongtu=1
- )
-
- logger.info(f"罚款创建成功: {fadan.id}")
- return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': fadan.id}})
-
- except Exception as e:
- logger.error(f"创建罚款异常: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '系统繁忙'})
-
-
-
-
-
-from dengji.models import Bankuai
-
-
-
-class HqbkxxView(APIView):
- """
- 获取板块配置所需全部数据
- POST /houtai/hqbkxx
- Body: { "phone": "管理员手机号" }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 从前端获取传递的账号,用于防越权校验
- username_from_frontend = request.data.get('phone', None)
-
- # 2. 调用公共验证方法
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
-
- # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值
- if kefu_obj is None:
- return permissions
-
- # ---------- 验证通过 ----------
- # 4. 检查是否有板块管理权限
- if 'bankuai' not in permissions:
- return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
-
- # 5. 查询所有板块
- bankuai_list = []
- for bk in Bankuai.objects.all():
- bankuai_list.append({
- 'bankuai_id': bk.bankuai_id,
- 'mingcheng': bk.mingcheng,
- 'create_time': bk.create_time.strftime('%Y-%m-%d %H:%M:%S') if bk.create_time else ''
- })
-
- # 6. 查询所有商品类型
- shangpin_leixing_list = []
- for sp in ShangpinLeixing.objects.all():
- shangpin_leixing_list.append({
- 'id': sp.id,
- 'jieshao': sp.jieshao,
- 'tupian_url': sp.tupian_url or '',
- 'bankuai_id': sp.bankuai_id
- })
-
- # 7. 查询所有会员
- huiyuan_list = []
- for hy in Huiyuan.objects.all():
- huiyuan_list.append({
- 'huiyuan_id': hy.huiyuan_id,
- 'jieshao': hy.jieshao,
- 'bankuai_id': hy.bankuai_id
- })
-
- return Response({
- 'code': 0,
- 'msg': 'ok',
- 'data': {
- 'bankuai': bankuai_list,
- 'shangpin_leixing': shangpin_leixing_list,
- 'huiyuan': huiyuan_list
- }
- })
-
-
-class BkxgView(APIView):
- """
- 板块增删改统一接口
- POST /houtai/bkxg
- Body: {
- "phone": "管理员手机号",
- "action": "create | update | delete | add_items | remove_items",
- "bankuai_id": int,
- "bankuai_name": str,
- "item_type": "shangpin_leixing | huiyuan",
- "item_ids": [1,2,3]
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 从前端获取传递的账号
- username_from_frontend = request.data.get('phone', None)
-
- # 2. 调用公共验证方法
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
-
- # 3. 验证失败直接返回
- if kefu_obj is None:
- return permissions
-
- # 4. 检查板块管理权限
- if 'bankuai' not in permissions:
- return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
-
- # 5. 获取操作类型
- action = request.data.get('action')
- if not action:
- return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
-
- # ==================== 创建板块 ====================
- if action == 'create':
- bankuai_name = request.data.get('bankuai_name', '').strip()
- if not bankuai_name:
- return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
- if Bankuai.objects.filter(mingcheng=bankuai_name).exists():
- return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
-
- shangpin_ids = request.data.get('shangpin_ids', [])
- huiyuan_ids = request.data.get('huiyuan_ids', [])
-
- with transaction.atomic():
- new_bk = Bankuai.objects.create(mingcheng=bankuai_name)
- if shangpin_ids:
- ShangpinLeixing.objects.filter(
- id__in=shangpin_ids,
- bankuai__isnull=True
- ).update(bankuai=new_bk)
- if huiyuan_ids:
- Huiyuan.objects.filter(
- huiyuan_id__in=huiyuan_ids,
- bankuai__isnull=True
- ).update(bankuai=new_bk)
-
- return Response({
- 'code': 0,
- 'msg': '创建成功',
- 'data': {'bankuai_id': new_bk.bankuai_id}
- })
-
- # ==================== 以下操作需要 bankuai_id ====================
- bankuai_id = request.data.get('bankuai_id')
- if not bankuai_id:
- return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400)
-
- try:
- bk = Bankuai.objects.get(bankuai_id=bankuai_id)
- except Bankuai.DoesNotExist:
- return Response({'code': 1, 'msg': '板块不存在'}, status=404)
-
- # ==================== 修改板块名称 ====================
- if action == 'update':
- new_name = request.data.get('bankuai_name', '').strip()
- if not new_name:
- return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
- if Bankuai.objects.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists():
- return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
- bk.mingcheng = new_name
- bk.save()
- return Response({'code': 0, 'msg': '修改成功'})
-
- # ==================== 删除板块 ====================
- elif action == 'delete':
- with transaction.atomic():
- ShangpinLeixing.objects.filter(bankuai=bk).update(bankuai=None)
- Huiyuan.objects.filter(bankuai=bk).update(bankuai=None)
- bk.delete()
- return Response({'code': 0, 'msg': '删除成功'})
-
- # ==================== 添加绑定 ====================
- elif action == 'add_items':
- item_type = request.data.get('item_type')
- item_ids = request.data.get('item_ids', [])
- if not item_type or not item_ids:
- return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
-
- if item_type == 'shangpin_leixing':
- ShangpinLeixing.objects.filter(
- id__in=item_ids,
- bankuai__isnull=True
- ).update(bankuai=bk)
- elif item_type == 'huiyuan':
- Huiyuan.objects.filter(
- huiyuan_id__in=item_ids,
- bankuai__isnull=True
- ).update(bankuai=bk)
- else:
- return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
- return Response({'code': 0, 'msg': '添加成功'})
-
- # ==================== 移除绑定 ====================
- elif action == 'remove_items':
- item_type = request.data.get('item_type')
- item_ids = request.data.get('item_ids', [])
- if not item_type or not item_ids:
- return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
-
- if item_type == 'shangpin_leixing':
- ShangpinLeixing.objects.filter(
- id__in=item_ids,
- bankuai=bk
- ).update(bankuai=None)
- elif item_type == 'huiyuan':
- Huiyuan.objects.filter(
- huiyuan_id__in=item_ids,
- bankuai=bk
- ).update(bankuai=None)
- else:
- return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
- return Response({'code': 0, 'msg': '移除成功'})
-
- else:
- return Response({'code': 1, 'msg': '未知操作'}, status=400)
-
-
-
-
-
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from django.db.models import Sum
-from django.utils import timezone
-from datetime import date, timedelta
-from peizhi.models import (
- DailyIncomeStat, DailyPayoutStat
-)
-from shangpin.models import (
- Czjilu,Huiyuangoumai
-)
-from .utils import verify_kefu_permission
-
-
-
-class CaiwuView(APIView):
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- username_from_frontend = request.data.get('phone', None)
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
-
- if kefu_obj is None:
- return permissions
-
- if 'caiwu' not in permissions:
- return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
-
- today = date.today()
- today_start = datetime.combine(today, datetime.min.time())
- today_end = today_start + timedelta(days=1)
-
- # 今日收入
- today_income = DailyIncomeStat.objects.filter(date=today).first()
- today_income_amount = float(today_income.total_amount) if today_income else 0.00
- today_income_count = today_income.total_count if today_income else 0
-
- # 今日支出
- today_payout = DailyPayoutStat.objects.filter(date=today).first()
- today_payout_amount = float(today_payout.total_amount) if today_payout else 0.00
- today_payout_count = today_payout.total_count if today_payout else 0
-
- # 今日订单(已付款及之后的状态)
- today_orders = Dingdan.objects.filter(
- create_time__gte=today_start,
- create_time__lt=today_end,
- zhuangtai__in=[1,2,3,4,5,6,7,8]
- )
- today_order_count = today_orders.count()
- today_order_amount = float(today_orders.aggregate(total=Sum('jine'))['total'] or 0.00)
-
- # 今日成交(zhuangtai=3)
- today_completed = Dingdan.objects.filter(
- create_time__gte=today_start,
- create_time__lt=today_end,
- zhuangtai=3
- )
- today_completed_count = today_completed.count()
- today_completed_amount = float(today_completed.aggregate(total=Sum('jine'))['total'] or 0.00)
-
- # 今日退款(zhuangtai=5)
- today_tuikuan = Dingdan.objects.filter(
- create_time__gte=today_start,
- create_time__lt=today_end,
- zhuangtai=5
- )
- today_tuikuan_count = today_tuikuan.count()
- today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('jine'))['total'] or 0.00)
-
- # 今日充值(会员充值,leixing=1, zhuangtai=3)
- today_chongzhi = Czjilu.objects.filter(
- create_time__gte=today_start,
- create_time__lt=today_end,
- leixing=1,
- zhuangtai=3
- )
- today_chongzhi_count = today_chongzhi.count()
- today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00)
-
- # 今日新增会员:从购买记录表统计今天创建的用户(去重)
- new_huiyuan_user_ids = Huiyuangoumai.objects.filter(
- create_time__gte=today_start,
- create_time__lt=today_end
- ).values_list('yonghu_id', flat=True).distinct()
- today_new_huiyuan = len(new_huiyuan_user_ids)
-
- # 新增会员充值总额(这些用户今天的会员充值金额)
- today_new_huiyuan_amount = 0.0
- if new_huiyuan_user_ids:
- amount_sum = Czjilu.objects.filter(
- create_time__gte=today_start,
- create_time__lt=today_end,
- yonghuid__in=new_huiyuan_user_ids,
- leixing=1,
- zhuangtai=3
- ).aggregate(total=Sum('jine'))
- today_new_huiyuan_amount = float(amount_sum['total'] or 0.0)
-
- # 各角色可提现余额总和
- all_guanshi_yue = float(UserGuanshi.objects.aggregate(total=Sum('yue'))['total'] or 0.00)
- all_dashou_yue = float(UserDashou.objects.aggregate(total=Sum('yue'))['total'] or 0.00)
- all_zuzhang_yue = float(UserZuzhang.objects.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00)
- all_shangjia_yue = float(UserShangjia.objects.aggregate(total=Sum('yue'))['total'] or 0.00)
-
- # 平台总收支及利润
- total_income = float(DailyIncomeStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00)
- total_payout = float(DailyPayoutStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00)
- platform_profit = round(total_income - total_payout, 2)
-
- # 近30天每日收支明细
- daily_stats = []
- income_qs = DailyIncomeStat.objects.order_by('-date')[:30]
- payout_dict = {
- str(p.date): float(p.total_amount)
- for p in DailyPayoutStat.objects.filter(date__gte=today - timedelta(days=30))
- }
- for inc in income_qs:
- d = str(inc.date)
- daily_stats.append({
- 'date': d,
- 'income': float(inc.total_amount),
- 'income_count': inc.total_count,
- 'payout': payout_dict.get(d, 0.00),
- 'profit': round(float(inc.total_amount) - payout_dict.get(d, 0.00), 2)
- })
-
- return Response({
- 'code': 0,
- 'msg': 'ok',
- 'data': {
- 'today_income': round(today_income_amount, 2),
- 'today_income_count': today_income_count,
- 'today_payout': round(today_payout_amount, 2),
- 'today_payout_count': today_payout_count,
- 'today_order_count': today_order_count,
- 'today_order_amount': round(today_order_amount, 2),
- 'today_completed_count': today_completed_count,
- 'today_completed_amount': round(today_completed_amount, 2),
- 'today_tuikuan_count': today_tuikuan_count,
- 'today_tuikuan_amount': round(today_tuikuan_amount, 2),
- 'today_chongzhi_count': today_chongzhi_count,
- 'today_chongzhi_amount': round(today_chongzhi_amount, 2),
- 'today_new_huiyuan': today_new_huiyuan,
- 'today_new_huiyuan_amount': round(today_new_huiyuan_amount, 2),
- 'all_guanshi_yue': round(all_guanshi_yue, 2),
- 'all_dashou_yue': round(all_dashou_yue, 2),
- 'all_zuzhang_yue': round(all_zuzhang_yue, 2),
- 'all_shangjia_yue': round(all_shangjia_yue, 2),
- 'total_income': round(total_income, 2),
- 'total_payout': round(total_payout, 2),
- 'platform_profit': platform_profit,
- 'daily_stats': daily_stats
- }
- })
-
-
-
-
-
-class CwhybkhqView(APIView):
- """
- 获取所有板块及对应会员列表
- POST /houtai/cwhybkhq
- Body: { "phone": "管理员手机号" }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- username_from_frontend = request.data.get('phone', None)
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
- if kefu_obj is None:
- return permissions
-
- if 'huiyuancaiwu' not in permissions:
- return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
-
- bankuai_list = []
- for bk in Bankuai.objects.all():
- members = Huiyuan.objects.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id')
- bankuai_list.append({
- 'bankuai_id': bk.bankuai_id,
- 'mingcheng': bk.mingcheng,
- 'members': list(members)
- })
-
- return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list})
- except Exception as e:
- logger.exception("CwhybkhqView 接口错误")
- return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
-
-from django.db.models.functions import TruncDate, TruncMonth, TruncYear
-from shangpin.models import Czjilu, Gsfenhong # 请根据实际路径调整
-class HybkjtsjView(APIView):
- """
- 会员详细统计数据
- POST /houtai/hybkjtsj
- Body: {
- "phone": "管理员手机号",
- "huiyuan_id": "会员ID",
- "granularity": "day" | "month" | "year",
- "year": 2026,
- "month": 5,
- "include_guanshi": true,
- "include_zuzhang": true,
- "summary_date": "2026-05-16"
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- username_from_frontend = request.data.get('phone', None)
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
- if kefu_obj is None:
- return permissions
-
- if 'huiyuancaiwu' not in permissions:
- return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
-
- huiyuan_id = request.data.get('huiyuan_id')
- granularity = request.data.get('granularity', 'day')
- year = request.data.get('year')
- month = request.data.get('month')
- include_guanshi = request.data.get('include_guanshi', False)
- include_zuzhang = request.data.get('include_zuzhang', False)
- summary_date = request.data.get('summary_date')
-
- if not huiyuan_id:
- return Response({'code': 1, 'msg': '缺少会员ID'}, status=400)
-
- # 🔧 直接使用 date.today() 避免模块名冲突
- from datetime import date, datetime, timedelta
- now = date.today()
-
- # ---------- 构造时间范围 ----------
- if granularity == 'day':
- if not year or not month:
- return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
- start_date = date(year, month, 1)
- if month == 12:
- end_date = date(year + 1, 1, 1)
- else:
- end_date = date(year, month + 1, 1)
- trunc_func = TruncDate('create_time')
- if not summary_date:
- summary_date = now.strftime('%Y-%m-%d')
- summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
- elif granularity == 'month':
- if not year:
- return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
- start_date = date(year, 1, 1)
- end_date = date(year + 1, 1, 1)
- trunc_func = TruncMonth('create_time')
- if not summary_date:
- summary_date = now.strftime('%Y-%m')
- summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
- elif granularity == 'year':
- year = year or now.year
- start_date = date(year, 1, 1)
- end_date = date(year + 1, 1, 1)
- trunc_func = TruncYear('create_time')
- if not summary_date:
- summary_date = str(year)
- summary_q = date(year, 1, 1)
- else:
- return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
-
- # 生成 naive datetime(避免 USE_TZ=False 报错)
- start_dt = datetime.combine(start_date, datetime.min.time())
- end_dt = datetime.combine(end_date, datetime.min.time())
-
- # ---------- 充值过滤 ----------
- chongzhi_filter = {
- 'huiyuan_id': huiyuan_id, # 会员ID筛选
- 'leixing': 1,
- 'zhuangtai': 3,
- 'create_time__gte': start_dt,
- 'create_time__lt': end_dt
- }
-
- # ---------- 分红过滤 ----------
- fenhong_filter = {
- 'huiyuan_id': huiyuan_id, # 分红表会员ID筛选
- 'create_time__gte': start_dt,
- 'create_time__lt': end_dt
- }
-
- # 充值分组查询
- chongzhi_qs = Czjilu.objects.filter(**chongzhi_filter) \
- .annotate(period=trunc_func) \
- .values('period') \
- .annotate(
- chongzhi_count=Count('id'),
- chongzhi_amount=Sum('jine')
- ).order_by('period')
-
- # 分红分组查询
- fenhong_qs = None
- if include_guanshi or include_zuzhang:
- fenhong_agg = {}
- if include_guanshi:
- fenhong_agg['guanshi_amount'] = Sum('fenhong')
- if include_zuzhang:
- fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong')
- fenhong_qs = Gsfenhong.objects.filter(**fenhong_filter) \
- .annotate(period=trunc_func) \
- .values('period') \
- .annotate(**fenhong_agg) \
- .order_by('period')
-
- # 合并数据
- data_map = {}
- for item in chongzhi_qs:
- period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
- item['period'].strftime('%Y-%m') if granularity == 'month' else \
- item['period'].strftime('%Y')
- data_map[period_str] = {
- 'chongzhi_count': item['chongzhi_count'],
- 'chongzhi_amount': float(item['chongzhi_amount'] or 0)
- }
-
- if fenhong_qs:
- for item in fenhong_qs:
- period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
- item['period'].strftime('%Y-%m') if granularity == 'month' else \
- item['period'].strftime('%Y')
- if period_str not in data_map:
- data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0}
- if include_guanshi:
- data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0)
- if include_zuzhang:
- data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0)
-
- time_series = []
- for period_str, vals in data_map.items():
- guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0
- zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0
- vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2)
- vals['date'] = period_str
- time_series.append(vals)
- time_series.sort(key=lambda x: x['date'])
-
- # ---------- 汇总数据 ----------
- summary_chongzhi_filter = chongzhi_filter.copy()
- if granularity == 'day':
- summary_chongzhi_filter['create_time__date'] = summary_q
- elif granularity == 'month':
- summary_chongzhi_filter['create_time__year'] = summary_q.year
- summary_chongzhi_filter['create_time__month'] = summary_q.month
- else:
- summary_chongzhi_filter['create_time__year'] = summary_q.year
-
- summary_chongzhi = Czjilu.objects.filter(**summary_chongzhi_filter).aggregate(
- total_count=Count('id'),
- total_amount=Sum('jine')
- )
- total_chongzhi_count = summary_chongzhi['total_count'] or 0
- total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0)
-
- total_guanshi = 0.0
- total_zuzhang = 0.0
- if include_guanshi or include_zuzhang:
- summary_fenhong_filter = fenhong_filter.copy()
- if granularity == 'day':
- summary_fenhong_filter['create_time__date'] = summary_q
- elif granularity == 'month':
- summary_fenhong_filter['create_time__year'] = summary_q.year
- summary_fenhong_filter['create_time__month'] = summary_q.month
- else:
- summary_fenhong_filter['create_time__year'] = summary_q.year
-
- fenhong_agg = {}
- if include_guanshi:
- fenhong_agg['guanshi_sum'] = Sum('fenhong')
- if include_zuzhang:
- fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong')
- summary_fenhong = Gsfenhong.objects.filter(**summary_fenhong_filter).aggregate(**fenhong_agg)
- total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0)
- total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0)
-
- total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2)
-
- return Response({
- 'code': 0,
- 'msg': 'ok',
- 'data': {
- 'time_series': time_series,
- 'summary': {
- 'total_chongzhi_count': total_chongzhi_count,
- 'total_chongzhi_amount': total_chongzhi_amount,
- 'total_guanshi_fenhong': total_guanshi,
- 'total_zuzhang_fenhong': total_zuzhang,
- 'total_shouyi': total_shouyi
- }
- }
- })
- except Exception as e:
- logger.exception("HybkjtsjView 接口错误")
- return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
-
-
-
-
-
-class SzxxView(APIView):
- """
- 每日收支详细统计数据
- POST /houtai/szxx
- Body: {
- "phone": "管理员手机号",
- "granularity": "day" | "month" | "year",
- "year": 2026, // day/month 时必填
- "month": 5, // day 时必填
- "summary_date": "2026-05-16" // 可选,格式随颗粒度
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 避免模块名冲突,局部导入
- from datetime import date, datetime
-
- username_from_frontend = request.data.get('phone', None)
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
- 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')
-
- now = date.today()
-
- # ---------- 时间范围与分组字段 ----------
- if granularity == 'day':
- if not year or not month:
- return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
- # 分组字段
- group_field = 'day'
- # 过滤条件
- income_filter = {'year': year, 'month': month}
- payout_filter = {'year': year, 'month': month}
- # 默认汇总日期
- if not summary_date:
- summary_date = now.strftime('%Y-%m-%d')
- summary_parts = summary_date.split('-')
- summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1]), 'day': int(summary_parts[2])}
- # 用于构造返回的日期字符串
- date_format = '%Y-%m-%d'
-
- elif granularity == 'month':
- if not year:
- return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
- group_field = 'month'
- income_filter = {'year': year}
- payout_filter = {'year': year}
- if not summary_date:
- summary_date = now.strftime('%Y-%m')
- summary_parts = summary_date.split('-')
- summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1])}
- date_format = '%Y-%m'
-
- elif granularity == 'year':
- group_field = 'year'
- income_filter = {}
- payout_filter = {}
- if not summary_date:
- summary_date = str(now.year)
- summary_filter = {'year': int(summary_date)}
- date_format = '%Y'
- else:
- return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
-
- # ---------- 收入分组查询 ----------
- income_qs = DailyIncomeStat.objects.filter(**income_filter) \
- .values(group_field) \
- .annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \
- .order_by(group_field)
-
- # ---------- 支出分组查询 ----------
- payout_qs = DailyPayoutStat.objects.filter(**payout_filter) \
- .values(group_field) \
- .annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) \
- .order_by(group_field)
-
- # ---------- 合并数据 ----------
- data_map = {}
- for item in income_qs:
- key = item[group_field]
- data_map[key] = {
- 'income': float(item['total_income'] or 0),
- 'income_count': item['total_count'] or 0,
- 'payout': 0.0,
- 'payout_count': 0
- }
-
- for item in payout_qs:
- key = item[group_field]
- if key not in data_map:
- data_map[key] = {'income': 0.0, 'income_count': 0, 'payout': 0.0, 'payout_count': 0}
- data_map[key]['payout'] = float(item['total_payout'] or 0)
- data_map[key]['payout_count'] = item['total_count'] or 0
-
- time_series = []
- for key, vals in data_map.items():
- # 构造日期字符串
- if granularity == 'day':
- date_str = f"{year}-{month:02d}-{key:02d}"
- elif granularity == 'month':
- date_str = f"{year}-{key:02d}"
- else:
- date_str = str(key)
- profit = round(vals['income'] - vals['payout'], 2)
- time_series.append({
- 'date': date_str,
- 'income': vals['income'],
- 'payout': vals['payout'],
- 'profit': profit
- })
- time_series.sort(key=lambda x: x['date'])
-
- # ---------- 汇总数据 ----------
- income_summary = DailyIncomeStat.objects.filter(**summary_filter).aggregate(
- total_income=Sum('total_amount'), total_count=Sum('total_count')
- )
- payout_summary = DailyPayoutStat.objects.filter(**summary_filter).aggregate(
- total_payout=Sum('total_amount'), total_count=Sum('total_count')
- )
- total_income = float(income_summary['total_income'] or 0)
- total_payout = float(payout_summary['total_payout'] or 0)
- total_profit = round(total_income - total_payout, 2)
-
- return Response({
- 'code': 0,
- 'msg': 'ok',
- 'data': {
- 'time_series': time_series,
- 'summary': {
- 'total_income': total_income,
- 'total_income_count': income_summary['total_count'] or 0,
- 'total_payout': total_payout,
- 'total_payout_count': payout_summary['total_count'] or 0,
- 'total_profit': total_profit
- }
- }
- })
-
- except Exception as e:
- logger.exception("SzxxView 接口错误")
- return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
-
-
-
-
-
-
-
-
-class CwddhqlxView(APIView):
- """
- 获取订单类型(商品类型)用于筛选
- POST /houtai/cwddhqlx
- Body: { "phone": "管理员手机号" }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- username_from_frontend = request.data.get('phone', None)
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
- if kefu_obj is None:
- return permissions
-
- if 'dingdancaiwu' not in permissions:
- return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
-
- # 获取非审核状态(shenhezhuangtai=1)的商品类型,只返回ID和名称
- types = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values('id', 'jieshao')
- return Response({
- 'code': 0,
- 'msg': 'ok',
- 'data': list(types)
- })
- except Exception as e:
- logger.exception("CwddhqlxView 接口错误")
- return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
-
-
-
-
-class CwhqjtddsjView(APIView):
- """
- 订单详细统计数据(修正版)
- POST /houtai/cwhqjtddsj
- Body: {
- "phone": "管理员手机号",
- "granularity": "day" | "month" | "year",
- "year": 2026,
- "month": 5,
- "type_id": 1, # 商品类型ID,0表示全部
- "order_source": 1, # 1=平台单,2=商家单,0=全部
- "summary_date": "2026-05-16"
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- username_from_frontend = request.data.get('phone', None)
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
- if kefu_obj is None:
- return permissions
- if 'dingdancaiwu' 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')
- type_id = request.data.get('type_id', 0)
- order_source = request.data.get('order_source', 0)
- summary_date = request.data.get('summary_date')
-
- now = date.today()
-
- # ---------- 时间范围 ----------
- if granularity == 'day':
- if not year or not month:
- return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
- start_date = date(year, month, 1)
- end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
- trunc_func = TruncDate('create_time')
- if not summary_date:
- summary_date = now.strftime('%Y-%m-%d')
- summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
- elif granularity == 'month':
- if not year:
- return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
- start_date = date(year, 1, 1)
- end_date = date(year + 1, 1, 1)
- trunc_func = TruncMonth('create_time')
- if not summary_date:
- summary_date = now.strftime('%Y-%m')
- summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
- elif granularity == 'year':
- year = year or now.year
- start_date = date(year, 1, 1)
- end_date = date(year + 1, 1, 1)
- trunc_func = TruncYear('create_time')
- if not summary_date:
- summary_date = str(year)
- summary_q = date(year, 1, 1)
- else:
- return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
-
- start_dt = datetime.combine(start_date, datetime.min.time())
- end_dt = datetime.combine(end_date, datetime.min.time())
-
- # ---------- 基础过滤 ----------
- filters = {
- 'create_time__gte': start_dt,
- 'create_time__lt': end_dt,
- 'zhuangtai__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态
- }
- if int(type_id) != 0:
- filters['leixing_id'] = type_id
- if int(order_source) in [1, 2]:
- filters['fadan_pingtai'] = order_source
-
- # ---------- 时间序列分组查询 ----------
- qs = Dingdan.objects.filter(**filters) \
- .annotate(period=trunc_func) \
- .values('period') \
- .annotate(
- # 订单总量(所有符合条件的状态)
- order_count=Count('id'),
- # 订单总额
- order_amount=Sum('jine'),
- # 成交单量 (zhuangtai=3)
- success_count=Count('id', filter=Q(zhuangtai=3)),
- # 成交订单总额
- success_amount=Sum('jine', filter=Q(zhuangtai=3)),
- # 退款单量 (zhuangtai=5)
- refund_count=Count('id', filter=Q(zhuangtai=5)),
- # 打手分成总额(仅成交订单)
- dashou_fencheng_sum=Sum('dashou_fencheng', filter=Q(zhuangtai=3)),
- # 平台成交订单的店铺分红
- dianpu_fenhong_sum=Sum('pingtai_kuozhan__dianpu_shouyi',
- filter=Q(zhuangtai=3, fadan_pingtai=1)),
- # 成交订单中:平台单金额、商家单金额、各自打手分成
- platform_success_amount=Sum('jine', filter=Q(zhuangtai=3, fadan_pingtai=1)),
- merchant_success_amount=Sum('jine', filter=Q(zhuangtai=3, fadan_pingtai=2)),
- platform_dashou=Sum('dashou_fencheng', filter=Q(zhuangtai=3, fadan_pingtai=1)),
- merchant_dashou=Sum('dashou_fencheng', filter=Q(zhuangtai=3, fadan_pingtai=2)),
- ).order_by('period')
-
- time_series = []
- for item in qs:
- period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
- item['period'].strftime('%Y-%m') if granularity == 'month' else \
- item['period'].strftime('%Y')
-
- # 平台收益计算
- platform_profit = (item['platform_success_amount'] or 0) \
- - (item['platform_dashou'] or 0) \
- - (item['dianpu_fenhong_sum'] or 0)
- merchant_profit = (item['merchant_success_amount'] or 0) \
- - (item['merchant_dashou'] or 0)
- total_profit = platform_profit + merchant_profit
-
- time_series.append({
- 'date': period_str,
- 'order_count': item['order_count'],
- 'order_amount': float(item['order_amount'] or 0),
- 'success_count': item['success_count'],
- 'success_amount': float(item['success_amount'] or 0),
- 'refund_count': item['refund_count'],
- 'dashou_fencheng': float(item['dashou_fencheng_sum'] or 0),
- 'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0),
- 'profit': round(total_profit, 2)
- })
-
- # ---------- 汇总数据 ----------
- summary_qs = Dingdan.objects.filter(**filters)
- total_order_count = summary_qs.count()
- total_order_amount = summary_qs.aggregate(s=Sum('jine'))['s'] or 0
- total_success_count = summary_qs.filter(zhuangtai=3).count()
- total_success_amount = summary_qs.filter(zhuangtai=3).aggregate(s=Sum('jine'))['s'] or 0
- total_refund_count = summary_qs.filter(zhuangtai=5).count()
- total_dashou = summary_qs.filter(zhuangtai=3).aggregate(s=Sum('dashou_fencheng'))['s'] or 0
- total_dianpu_fenhong = summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(
- s=Sum('pingtai_kuozhan__dianpu_shouyi'))['s'] or 0
-
- platform_amount = summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(s=Sum('jine'))['s'] or 0
- merchant_amount = summary_qs.filter(zhuangtai=3, fadan_pingtai=2).aggregate(s=Sum('jine'))['s'] or 0
- platform_dashou_sum = \
- summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(s=Sum('dashou_fencheng'))['s'] or 0
- merchant_dashou_sum = \
- summary_qs.filter(zhuangtai=3, fadan_pingtai=2).aggregate(s=Sum('dashou_fencheng'))['s'] or 0
-
- profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
- profit_merchant = merchant_amount - merchant_dashou_sum
- total_profit = profit_platform + profit_merchant
-
- summary = {
- 'total_order_count': total_order_count,
- 'total_order_amount': round(float(total_order_amount), 2),
- 'total_success_count': total_success_count,
- 'total_success_amount': round(float(total_success_amount), 2),
- 'total_refund_count': total_refund_count,
- 'total_dashou_fencheng': round(float(total_dashou), 2),
- 'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2),
- 'total_profit': round(total_profit, 2)
- }
-
- return Response({
- 'code': 0,
- 'msg': 'ok',
- 'data': {
- 'time_series': time_series,
- 'summary': summary
- }
- })
- except Exception as e:
- logger.exception("CwhqjtddsjView 接口错误")
- return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
-
-
-
-
-
-
-
-
-
-
-from dingdan.models import Fadan, FadanFenhong # 罚单和罚款分红表,根据实际路径调整
-import logging
-from datetime import date, datetime
-from django.db.models import Count, Sum, Q
-
-
-
-from django.db.models.functions import TruncDate, TruncMonth, TruncYear
-
-
-class CwqtczhqView(APIView):
- """
- 其他充值/罚款数据统计(非会员充值)
- POST /houtai/cwqtczhq
- Body: {
- "phone": "管理员手机号",
- "granularity": "day" | "month" | "year",
- "year": 2026,
- "month": 5, // day时必填
- "types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
- "summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- username_from_frontend = request.data.get('phone', None)
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
- if kefu_obj is None:
- return permissions
- if 'qtczcaiwu' 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')
- types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部
- summary_date = request.data.get('summary_date')
-
- if isinstance(types, list) and len(types) == 0:
- types = [2, 3, 4, 5, 6]
-
- now = date.today()
-
- # ---------- 时间范围(用于曲线图) ----------
- if granularity == 'day':
- if not year or not month:
- return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
- start_date = date(year, month, 1)
- end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
- trunc_func = TruncDate('create_time')
- # 默认汇总日期
- if not summary_date:
- summary_date = now.strftime('%Y-%m-%d')
- summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
- elif granularity == 'month':
- if not year:
- return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
- start_date = date(year, 1, 1)
- end_date = date(year + 1, 1, 1)
- trunc_func = TruncMonth('create_time')
- if not summary_date:
- summary_date = now.strftime('%Y-%m')
- summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
- elif granularity == 'year':
- year = year or now.year
- start_date = date(year, 1, 1)
- end_date = date(year + 1, 1, 1)
- trunc_func = TruncYear('create_time')
- if not summary_date:
- summary_date = str(year)
- summary_q = date(year, 1, 1)
- else:
- return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
-
- start_dt = datetime.combine(start_date, datetime.min.time())
- end_dt = datetime.combine(end_date, datetime.min.time())
-
- # ---------- 构建时间序列 ----------
- time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} }
-
- def ensure_date(ds):
- if ds not in time_series_map:
- time_series_map[ds] = {}
- return time_series_map[ds]
-
- # 1. 处理充值记录(Czjilu)—— 完全不变
- cz_types = [t for t in types if t in [2, 3, 4, 5]]
- if cz_types:
- cz_qs = Czjilu.objects.filter(
- create_time__gte=start_dt,
- create_time__lt=end_dt,
- leixing__in=cz_types,
- zhuangtai=3 # 只统计支付成功的
- ).annotate(
- period=trunc_func
- ).values('period', 'leixing').annotate(
- total_count=Count('id'),
- total_amount=Sum('jine')
- ).order_by('period', 'leixing')
-
- for item in cz_qs:
- period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
- item['period'].strftime('%Y-%m') if granularity == 'month' else \
- item['period'].strftime('%Y')
- lx = item['leixing']
- amount = float(item['total_amount'] or 0)
- count = item['total_count']
- # 收益:积分(3)收益为金额本身,其他为0
- profit = amount if lx == 3 else 0.0
-
- day_data = ensure_date(period_str)
- day_data[str(lx)] = {
- 'count': count,
- 'amount': amount,
- 'profit': profit,
- 'fenhong_count': 0, # 充值类型无分红
- 'fenhong_amount': 0.0
- }
-
- # 2. 处理罚款(Fadan)—— 修改:直接从 Fadan 表获取分红总额和分红笔数
- if 6 in types:
- # 使用一次聚合查询,同时获取罚款笔数、罚款金额、分红总额、分红笔数
- penalty_qs = Fadan.objects.filter(
- create_time__gte=start_dt,
- create_time__lt=end_dt,
- zhuangtai=2 # 已缴纳
- ).annotate(
- period=trunc_func
- ).values('period').annotate(
- penalty_count=Count('id'),
- penalty_amount=Sum('fakuanjine'),
- # 直接使用罚单表中的申请人分红金额字段
- fenhong_amount=Sum('shenqingren_fenhong_jine'),
- fenhong_count=Count('id', filter=Q(shenqingren_fenhong_jine__gt=0))
- ).order_by('period')
-
- for item in penalty_qs:
- period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
- item['period'].strftime('%Y-%m') if granularity == 'month' else \
- item['period'].strftime('%Y')
- day_data = ensure_date(period_str)
- penalty_amt = float(item['penalty_amount'] or 0)
- fenhong_amt = float(item['fenhong_amount'] or 0)
- profit = round(penalty_amt - fenhong_amt, 2)
- day_data['6'] = {
- 'count': item['penalty_count'],
- 'amount': penalty_amt,
- 'profit': profit,
- 'fenhong_count': item['fenhong_count'],
- 'fenhong_amount': fenhong_amt
- }
-
- # 构造有序时间序列列表
- time_series = []
- for period_str in sorted(time_series_map.keys()):
- entry = {'date': period_str, 'types': {}}
- for type_code in sorted(time_series_map[period_str].keys()):
- entry['types'][type_code] = time_series_map[period_str][type_code]
- time_series.append(entry)
-
- # ---------- 汇总指定日期(summary)—— 完全保留原逻辑,仅将罚款部分改为从 Fadan 表直接取分红 ----------
- summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
-
- # 汇总充值类型(完全不变)
- for lx in cz_types:
- filter_kwargs = {
- 'leixing': lx,
- 'zhuangtai': 3,
- }
- if granularity == 'day':
- filter_kwargs['create_time__date'] = summary_q
- elif granularity == 'month':
- filter_kwargs['create_time__year'] = summary_q.year
- filter_kwargs['create_time__month'] = summary_q.month
- else:
- filter_kwargs['create_time__year'] = summary_q.year
- agg = Czjilu.objects.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine'))
- count = agg['count'] or 0
- amount = float(agg['amount'] or 0)
- profit = amount if lx == 3 else 0.0
- summary_result['by_type'][str(lx)] = {
- 'count': count,
- 'amount': round(amount, 2),
- 'profit': round(profit, 2),
- 'fenhong_count': 0,
- 'fenhong_amount': 0.0
- }
- summary_result['total_count'] += count
- summary_result['total_amount'] += amount
- summary_result['total_profit'] += profit
-
- # 汇总罚款(修改:从 Fadan 表直接取分红,不再查询 FadanFenhong)
- if 6 in types:
- penalty_filter = {
- 'zhuangtai': 2,
- }
- if granularity == 'day':
- penalty_filter['create_time__date'] = summary_q
- elif granularity == 'month':
- penalty_filter['create_time__year'] = summary_q.year
- penalty_filter['create_time__month'] = summary_q.month
- else:
- penalty_filter['create_time__year'] = summary_q.year
-
- # 一次聚合:罚款笔数、金额、分红总额、分红笔数
- agg = Fadan.objects.filter(**penalty_filter).aggregate(
- count=Count('id'),
- amount=Sum('fakuanjine'),
- fenhong_amount=Sum('shenqingren_fenhong_jine'),
- fenhong_count=Count('id', filter=Q(shenqingren_fenhong_jine__gt=0))
- )
- penalty_count = agg['count'] or 0
- penalty_amount = float(agg['amount'] or 0)
- fh_amount = float(agg['fenhong_amount'] or 0)
- fh_count = agg['fenhong_count'] or 0
- profit = round(penalty_amount - fh_amount, 2)
-
- summary_result['by_type']['6'] = {
- 'count': penalty_count,
- 'amount': round(penalty_amount, 2),
- 'profit': profit,
- 'fenhong_count': fh_count,
- 'fenhong_amount': round(fh_amount, 2)
- }
- summary_result['total_count'] += penalty_count
- summary_result['total_amount'] += penalty_amount
- summary_result['total_profit'] += profit
-
- summary_result['total_amount'] = round(summary_result['total_amount'], 2)
- summary_result['total_profit'] = round(summary_result['total_profit'], 2)
-
- return Response({
- 'code': 0,
- 'msg': 'ok',
- 'data': {
- 'time_series': time_series,
- 'summary': summary_result
- }
- })
- except Exception as e:
- logger.exception("CwqtczhqView 接口错误")
- return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
-
-'''class CwqtczhqView(APIView):
- """
- 其他充值/罚款数据统计(非会员充值)
- POST /houtai/cwqtczhq
- Body: {
- "phone": "管理员手机号",
- "granularity": "day" | "month" | "year",
- "year": 2026,
- "month": 5, // day时必填
- "types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
- "summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- username_from_frontend = request.data.get('phone', None)
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
- if kefu_obj is None:
- return permissions
- if 'qtczcaiwu' 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')
- types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部
- summary_date = request.data.get('summary_date')
-
- if isinstance(types, list) and len(types) == 0:
- types = [2, 3, 4, 5, 6]
-
- now = date.today()
-
- # ---------- 时间范围(用于曲线图) ----------
- if granularity == 'day':
- if not year or not month:
- return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
- start_date = date(year, month, 1)
- end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
- trunc_func = TruncDate('create_time')
- # 默认汇总日期
- if not summary_date:
- summary_date = now.strftime('%Y-%m-%d')
- summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
- elif granularity == 'month':
- if not year:
- return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
- start_date = date(year, 1, 1)
- end_date = date(year + 1, 1, 1)
- trunc_func = TruncMonth('create_time')
- if not summary_date:
- summary_date = now.strftime('%Y-%m')
- summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
- elif granularity == 'year':
- year = year or now.year
- start_date = date(year, 1, 1)
- end_date = date(year + 1, 1, 1)
- trunc_func = TruncYear('create_time')
- if not summary_date:
- summary_date = str(year)
- summary_q = date(year, 1, 1)
- else:
- return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
-
- start_dt = datetime.combine(start_date, datetime.min.time())
- end_dt = datetime.combine(end_date, datetime.min.time())
-
- # ---------- 构建时间序列 ----------
- time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} }
-
- def ensure_date(ds):
- if ds not in time_series_map:
- time_series_map[ds] = {}
- return time_series_map[ds]
-
- # 1. 处理充值记录(Czjilu)
- cz_types = [t for t in types if t in [2, 3, 4, 5]]
- if cz_types:
- cz_qs = Czjilu.objects.filter(
- create_time__gte=start_dt,
- create_time__lt=end_dt,
- leixing__in=cz_types,
- zhuangtai=3 # 只统计支付成功的
- ).annotate(
- period=trunc_func
- ).values('period', 'leixing').annotate(
- total_count=Count('id'),
- total_amount=Sum('jine')
- ).order_by('period', 'leixing')
-
- for item in cz_qs:
- period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
- item['period'].strftime('%Y-%m') if granularity == 'month' else \
- item['period'].strftime('%Y')
- lx = item['leixing']
- amount = float(item['total_amount'] or 0)
- count = item['total_count']
- # 收益:积分(3)收益为金额本身,其他为0
- profit = amount if lx == 3 else 0.0
-
- day_data = ensure_date(period_str)
- day_data[str(lx)] = {
- 'count': count,
- 'amount': amount,
- 'profit': profit,
- 'fenhong_count': 0, # 充值类型无分红
- 'fenhong_amount': 0.0
- }
-
- # 2. 处理罚款(Fadan)—— 包含分红
- if 6 in types:
- # 罚款记录
- penalty_qs = Fadan.objects.filter(
- create_time__gte=start_dt,
- create_time__lt=end_dt,
- zhuangtai=2 # 已缴纳
- ).annotate(
- period=trunc_func
- ).values('period').annotate(
- penalty_count=Count('id'),
- penalty_amount=Sum('fakuanjine')
- ).order_by('period')
-
- # 分红记录
- fenhong_qs = FadanFenhong.objects.filter(
- create_time__gte=start_dt,
- create_time__lt=end_dt
- ).annotate(
- period=trunc_func
- ).values('period').annotate(
- fh_count=Count('id'),
- fh_amount=Sum('ketixian_fenhong')
- ).order_by('period')
-
- # 合并
- for item in penalty_qs:
- period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
- item['period'].strftime('%Y-%m') if granularity == 'month' else \
- item['period'].strftime('%Y')
- day_data = ensure_date(period_str)
- day_data['6'] = {
- 'count': item['penalty_count'],
- 'amount': float(item['penalty_amount'] or 0),
- 'profit': 0.0, # 先设为0,后续计算
- 'fenhong_count': 0,
- 'fenhong_amount': 0.0
- }
-
- for item in fenhong_qs:
- period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
- item['period'].strftime('%Y-%m') if granularity == 'month' else \
- item['period'].strftime('%Y')
- day_data = ensure_date(period_str)
- if '6' not in day_data:
- day_data['6'] = {'count': 0, 'amount': 0.0, 'profit': 0.0, 'fenhong_count': 0, 'fenhong_amount': 0.0}
- day_data['6']['fenhong_count'] += item['fh_count']
- day_data['6']['fenhong_amount'] += float(item['fh_amount'] or 0)
-
- # 计算罚款收益 = 罚款金额 - 分红金额
- for period_str, day_data in time_series_map.items():
- if '6' in day_data:
- penalty = day_data['6']
- penalty['profit'] = round(penalty['amount'] - penalty['fenhong_amount'], 2)
-
- # 构造有序时间序列列表
- time_series = []
- for period_str in sorted(time_series_map.keys()):
- entry = {'date': period_str, 'types': {}}
- for type_code in sorted(time_series_map[period_str].keys()):
- entry['types'][type_code] = time_series_map[period_str][type_code]
- time_series.append(entry)
-
- # ---------- 汇总指定日期 ----------
- summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
-
- # 汇总充值类型
- for lx in cz_types:
- filter_kwargs = {
- 'leixing': lx,
- 'zhuangtai': 3,
- }
- if granularity == 'day':
- filter_kwargs['create_time__date'] = summary_q
- elif granularity == 'month':
- filter_kwargs['create_time__year'] = summary_q.year
- filter_kwargs['create_time__month'] = summary_q.month
- else:
- filter_kwargs['create_time__year'] = summary_q.year
- agg = Czjilu.objects.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine'))
- count = agg['count'] or 0
- amount = float(agg['amount'] or 0)
- profit = amount if lx == 3 else 0.0
- summary_result['by_type'][str(lx)] = {
- 'count': count,
- 'amount': round(amount, 2),
- 'profit': round(profit, 2),
- 'fenhong_count': 0,
- 'fenhong_amount': 0.0
- }
- summary_result['total_count'] += count
- summary_result['total_amount'] += amount
- summary_result['total_profit'] += profit
-
- # 汇总罚款
- if 6 in types:
- penalty_filter = {
- 'zhuangtai': 2,
- }
- fenhong_filter = {}
- if granularity == 'day':
- penalty_filter['create_time__date'] = summary_q
- fenhong_filter['create_time__date'] = summary_q
- elif granularity == 'month':
- penalty_filter['create_time__year'] = summary_q.year
- penalty_filter['create_time__month'] = summary_q.month
- fenhong_filter['create_time__year'] = summary_q.year
- fenhong_filter['create_time__month'] = summary_q.month
- else:
- penalty_filter['create_time__year'] = summary_q.year
- fenhong_filter['create_time__year'] = summary_q.year
-
- penalty_agg = Fadan.objects.filter(**penalty_filter).aggregate(
- count=Count('id'), amount=Sum('fakuanjine'))
- penalty_count = penalty_agg['count'] or 0
- penalty_amount = float(penalty_agg['amount'] or 0)
-
- fenhong_agg = FadanFenhong.objects.filter(**fenhong_filter).aggregate(
- fh_count=Count('id'), fh_amount=Sum('ketixian_fenhong'))
- fh_count = fenhong_agg['fh_count'] or 0
- fh_amount = float(fenhong_agg['fh_amount'] or 0)
-
- profit = round(penalty_amount - fh_amount, 2)
- summary_result['by_type']['6'] = {
- 'count': penalty_count,
- 'amount': round(penalty_amount, 2),
- 'profit': profit,
- 'fenhong_count': fh_count,
- 'fenhong_amount': round(fh_amount, 2)
- }
- summary_result['total_count'] += penalty_count
- summary_result['total_amount'] += penalty_amount
- summary_result['total_profit'] += profit
-
- summary_result['total_amount'] = round(summary_result['total_amount'], 2)
- summary_result['total_profit'] = round(summary_result['total_profit'], 2)
-
- return Response({
- 'code': 0,
- 'msg': 'ok',
- 'data': {
- 'time_series': time_series,
- 'summary': summary_result
- }
- })
- except Exception as e:
- logger.exception("CwqtczhqView 接口错误")
- return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)'''
-
-
-
-
-
-# ==================== 后端接口 ====================
-# 文件:views.py (添加以下两个视图类)
-
-from dengji.models import (
- Chenghao, KaoheCishuFeiyong, Bankuai,
- YonghuChenghao, ShenheJilu, # 可能不需要用到但导入无妨
-)
-
-
-
-class KhpzhqView(APIView):
- """
- 获取所有考核配置数据(称号列表、板块、费用)
- POST /houtai/khpzhq
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- username_from_frontend = request.data.get('phone', None)
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
- if kefu_obj is None:
- return permissions
- if 'kaohepeizhi' not in permissions:
- return Response({'code': 403, 'msg': '无考核配置权限'}, status=403)
-
- bankuai_list = list(Bankuai.objects.values('bankuai_id', 'mingcheng'))
-
- chenghao_qs = Chenghao.objects.prefetch_related('kaohecishufeiyong_set').all()
- chenghao_list = []
- for ch in chenghao_qs:
- # 获取费用:按次数排序,转为 {cishu: feiyong} 字典
- feiyong_dict = {
- item.cishu: float(item.feiyong)
- for item in ch.kaohecishufeiyong_set.order_by('cishu')
- }
- max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0
-
- chenghao_list.append({
- 'id': ch.id,
- 'mingcheng': ch.mingcheng,
- 'leixing': ch.leixing,
- 'leixing_name': ch.get_leixing_display(),
- 'bankuai_id': ch.bankuai_id,
- 'tubiao_url': ch.tubiao_url or '',
- 'texiao_miaoshu': ch.texiao_miaoshu or '',
- 'create_time': ch.create_time.strftime('%Y-%m-%d %H:%M:%S') if ch.create_time else '',
- 'kaohe_guize': ch.kaohe_guize or '',
- 'kaioi_jinpai': ch.kaioi_jinpai,
- 'feiyong': feiyong_dict, # {1:100.0, 2:200.0}
- 'max_cishu': max_cishu
- })
-
- return Response({
- 'code': 0,
- 'msg': 'ok',
- 'data': {
- 'bankuai': bankuai_list,
- 'chenghao': chenghao_list
- }
- })
- except Exception as e:
- logger.exception("KhpzhqView 接口错误")
- return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
-
-
-
-
-class ChzsgcView(APIView):
- """
- 称号(标签)增删改查统一接口
- POST /houtai/chzsgc
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- username_from_frontend = request.data.get('phone', None)
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
- if kefu_obj is None:
- return permissions
- if 'kaohepeizhi' not in permissions:
- return Response({'code': 403, 'msg': '无考核配置权限'}, status=403)
-
- action = request.data.get('action')
- if not action:
- return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
-
- # ========== 创建称号 ==========
- if action == 'create':
- mingcheng = request.data.get('mingcheng', '').strip()
- leixing = request.data.get('leixing', 'dashou')
- bankuai_id = request.data.get('bankuai_id')
- texiao_miaoshu = request.data.get('texiao_miaoshu', '')
- kaohe_guize = request.data.get('kaohe_guize', '')
- kaioi_jinpai = request.data.get('kaioi_jinpai', False)
- feiyong_list = request.data.get('feiyong_list', [])
-
- if not mingcheng:
- return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
- if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
- return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
- if Chenghao.objects.filter(mingcheng=mingcheng).exists():
- return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
-
- with transaction.atomic():
- ch = Chenghao.objects.create(
- mingcheng=mingcheng,
- leixing=leixing,
- bankuai_id=bankuai_id,
- texiao_miaoshu=texiao_miaoshu,
- kaohe_guize=kaohe_guize,
- kaioi_jinpai=kaioi_jinpai
- )
- # 处理费用:前端传来 [{cishu:1, feiyong:100}, ...]
- if feiyong_list:
- for item in feiyong_list:
- cishu = int(item.get('cishu', 1))
- feiyong = float(item.get('feiyong', 0))
- KaoheCishuFeiyong.objects.update_or_create(
- chenghao=ch, cishu=cishu,
- defaults={'feiyong': feiyong}
- )
- else:
- # 默认添加第一次免费
- KaoheCishuFeiyong.objects.create(chenghao=ch, cishu=1, feiyong=0)
-
- return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}})
-
- # ========== 修改称号 ==========
- elif action == 'update':
- ch_id = request.data.get('chenghao_id')
- if not ch_id:
- return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
- try:
- ch = Chenghao.objects.get(id=ch_id)
- except Chenghao.DoesNotExist:
- return Response({'code': 1, 'msg': '称号不存在'}, status=404)
-
- mingcheng = request.data.get('mingcheng', ch.mingcheng).strip()
- leixing = request.data.get('leixing', ch.leixing)
- bankuai_id = request.data.get('bankuai_id', ch.bankuai_id)
- texiao_miaoshu = request.data.get('texiao_miaoshu', ch.texiao_miaoshu)
- kaohe_guize = request.data.get('kaohe_guize', ch.kaohe_guize)
- kaioi_jinpai = request.data.get('kaioi_jinpai', ch.kaioi_jinpai)
-
- if not mingcheng:
- return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
- if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
- return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
- if Chenghao.objects.filter(mingcheng=mingcheng).exclude(id=ch_id).exists():
- return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
-
- ch.mingcheng = mingcheng
- ch.leixing = leixing
- ch.bankuai_id = bankuai_id
- ch.texiao_miaoshu = texiao_miaoshu
- ch.kaohe_guize = kaohe_guize
- ch.kaioi_jinpai = kaioi_jinpai
- ch.save()
-
- # 如果前端传递了 feiyong_list,则全量替换费用
- feiyong_list = request.data.get('feiyong_list')
- if feiyong_list is not None:
- with transaction.atomic():
- ch.kaohecishufeiyong_set.all().delete()
- for item in feiyong_list:
- KaoheCishuFeiyong.objects.create(
- chenghao=ch,
- cishu=int(item.get('cishu', 1)),
- feiyong=float(item.get('feiyong', 0))
- )
-
- return Response({'code': 0, 'msg': '修改成功'})
-
- # ========== 删除称号 ==========
- elif action == 'delete':
- ch_id = request.data.get('chenghao_id')
- if not ch_id:
- return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
- try:
- ch = Chenghao.objects.get(id=ch_id)
- except Chenghao.DoesNotExist:
- return Response({'code': 1, 'msg': '称号不存在'}, status=404)
- ch.delete()
- return Response({'code': 0, 'msg': '删除成功'})
-
- # ========== 添加/修改某次费用 ==========
- elif action == 'add_feiyong':
- ch_id = request.data.get('chenghao_id')
- cishu = request.data.get('cishu')
- feiyong = request.data.get('feiyong', 0)
- if not ch_id or not cishu:
- return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
- try:
- ch = Chenghao.objects.get(id=ch_id)
- except Chenghao.DoesNotExist:
- return Response({'code': 1, 'msg': '称号不存在'}, status=404)
- if int(cishu) < 1:
- return Response({'code': 1, 'msg': '次数必须>=1'}, status=400)
- KaoheCishuFeiyong.objects.update_or_create(
- chenghao=ch, cishu=cishu,
- defaults={'feiyong': feiyong}
- )
- return Response({'code': 0, 'msg': '添加/修改费用成功'})
-
- # ========== 删除某次费用 ==========
- elif action == 'remove_feiyong':
- ch_id = request.data.get('chenghao_id')
- cishu = request.data.get('cishu')
- if not ch_id or not cishu:
- return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
- if int(cishu) == 1:
- return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400)
- try:
- ch = Chenghao.objects.get(id=ch_id)
- except Chenghao.DoesNotExist:
- return Response({'code': 1, 'msg': '称号不存在'}, status=404)
- KaoheCishuFeiyong.objects.filter(chenghao=ch, cishu=cishu).delete()
- return Response({'code': 0, 'msg': '删除成功'})
-
- # ========== 修改某次费用(等价于add) ==========
- elif action == 'update_feiyong':
- return self.add_feiyong(request) # 复用逻辑
-
- else:
- return Response({'code': 1, 'msg': '未知操作'}, status=400)
-
- except Exception as e:
- logger.exception("ChzsgcView 接口错误")
- return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
-
-
-
-
-# ==================== 后端 views.py ====================
-# 请确保已有以下导入,如果没有请在文件头部添加
-
-from yonghu.models import (
- UserMain, UserShenheguan
-)
-
-from dengji.models import (
- KaoheguanBankuai, Bankuai
-)
-
-
-class KhgglView(APIView):
- """
- 考核官管理数据接口(列表、筛选、分页)
- POST /houtai/khggl
- Body: {
- "phone": "管理员手机号",
- "page": 1, // 可选,默认1
- "page_size": 15, // 可选,默认15
- "yonghuid": "1234567", // 按用户ID筛选,可选
- "zhuangtai": 1, // 账号状态 0=禁用,1=正常,可选
- "shenhe_zhuangtai": 0, // 审核官状态 0=空闲,1=审核中,可选
- "bankuai_name": "王者" // 板块名称模糊筛选,可选
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- username_from_frontend = request.data.get('phone', None)
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
- if kefu_obj is None:
- return permissions
-
- if 'kaohepeizhi' not in permissions:
- return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
-
- # 分页参数
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 15))
- if page < 1: page = 1
- if page_size < 1: page_size = 15
-
- # 筛选条件
- filter_yonghuid = request.data.get('yonghuid', '').strip()
- filter_zhuangtai = request.data.get('zhuangtai')
- filter_shenhe_zhuangtai = request.data.get('shenhe_zhuangtai')
- filter_bankuai_name = request.data.get('bankuai_name', '').strip()
-
- # 基础查询:所有审核官,预加载用户及打手扩展表
- queryset = UserShenheguan.objects.select_related('user__dashou_profile')
-
- if filter_yonghuid:
- queryset = queryset.filter(user__yonghuid=filter_yonghuid)
- if filter_zhuangtai is not None:
- queryset = queryset.filter(zhuangtai=filter_zhuangtai)
- if filter_shenhe_zhuangtai is not None:
- queryset = queryset.filter(shenhe_zhuangtai=filter_shenhe_zhuangtai)
-
- # 如果填写了板块名称,筛选出关联该板块的审核官
- if filter_bankuai_name:
- bankuai_ids = Bankuai.objects.filter(
- mingcheng__icontains=filter_bankuai_name
- ).values_list('bankuai_id', flat=True)
- shenheguan_ids = KaoheguanBankuai.objects.filter(
- bankuai_id__in=bankuai_ids
- ).values_list('kaoheguan_id', flat=True)
- queryset = queryset.filter(user__yonghuid__in=shenheguan_ids)
-
- total = queryset.count()
- start = (page - 1) * page_size
- end = start + page_size
- shenheguan_list = queryset.order_by('-create_time')[start:end]
-
- # ---------- 预计算每个板块的打手数量 ----------
- bankuai_dashou_count_map = {}
- all_banquai = Bankuai.objects.all()
- for bk in all_banquai:
- tag_ids = Chenghao.objects.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True)
- count = YonghuChenghao.objects.filter(
- chenghao_id__in=tag_ids,
- yonghu__dashou_profile__isnull=False
- ).values('yonghu').distinct().count()
- bankuai_dashou_count_map[bk.bankuai_id] = count
-
- # ---------- 新增:板块标签统计 ----------
- bankuai_tags_data = []
- for bk in all_banquai:
- tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou')
- tag_list = []
- for tag in tags:
- user_count = YonghuChenghao.objects.filter(chenghao=tag).values('yonghu').distinct().count()
- tag_list.append({
- 'id': tag.id,
- 'name': tag.mingcheng,
- 'user_count': user_count,
- })
- bankuai_tags_data.append({
- 'bankuai_id': bk.bankuai_id,
- 'mingcheng': bk.mingcheng,
- 'tags': tag_list,
- })
-
- # 构造返回数据(审核官列表)
- data_list = []
- for sg in shenheguan_list:
- user = sg.user
- # 获取该审核官关联的板块
- bk_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=user.yonghuid).select_related('bankuai')
- bankuai_info = []
- for bk in bk_qs:
- bid = bk.bankuai.bankuai_id
- bankuai_info.append({
- 'bankuai_id': bid,
- 'mingcheng': bk.bankuai.mingcheng,
- 'dashou_count': bankuai_dashou_count_map.get(bid, 0)
- })
-
- dashou_nick = user.dashou_profile.nicheng if hasattr(user, 'dashou_profile') and user.dashou_profile else ''
-
- data_list.append({
- 'yonghuid': user.yonghuid,
- 'avatar': user.avatar or '',
- 'phone': user.phone or '',
- 'zhuangtai': sg.zhuangtai,
- 'shenhe_zhuangtai': sg.shenhe_zhuangtai,
- 'shenhe_zongshu': sg.shenhe_zongshu,
- 'tongguo_zongshu': sg.tongguo_zongshu,
- 'yue': float(sg.yue),
- 'zonge': float(sg.zonge),
- 'create_time': sg.create_time.strftime('%Y-%m-%d %H:%M:%S') if sg.create_time else '',
- 'update_time': sg.update_time.strftime('%Y-%m-%d %H:%M:%S') if sg.update_time else '',
- 'bankuai': bankuai_info,
- 'nicheng': dashou_nick,
- 'is_renzheng': sg.is_renzheng,
- })
-
- all_bankuai = list(Bankuai.objects.values('bankuai_id', 'mingcheng'))
-
- return Response({
- 'code': 0,
- 'msg': 'ok',
- 'data': {
- 'total': total,
- 'page': page,
- 'page_size': page_size,
- 'list': data_list,
- 'all_bankuai': all_bankuai,
- 'bankuai_tags': bankuai_tags_data, # 新增板块标签统计
- }
- })
-
- except Exception as e:
- logger.exception("KhgglView 接口错误")
- return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
-
-class ShgxgsjView(APIView):
- """
- 审核官信息修改(余额、状态、板块关联)
- POST /houtai/shgxgsj
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- username_from_frontend = request.data.get('phone', None)
- kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
- if kefu_obj is None:
- return permissions
- if 'kaohepeizhi' not in permissions:
- return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
-
- yonghuid = request.data.get('yonghuid')
- if not yonghuid:
- return Response({'code': 1, 'msg': '缺少用户ID'}, status=400)
-
- try:
- shenheguan = UserShenheguan.objects.select_related('user').get(user__yonghuid=yonghuid)
- except UserShenheguan.DoesNotExist:
- return Response({'code': 1, 'msg': '审核官不存在'}, status=404)
-
- action = request.data.get('action')
-
- if action == 'update_info':
- if 'yue' in request.data:
- shenheguan.yue = float(request.data.get('yue', 0))
- if 'zhuangtai' in request.data:
- zt = int(request.data.get('zhuangtai'))
- if zt not in [0, 1]:
- return Response({'code': 1, 'msg': '无效的账号状态'}, status=400)
- shenheguan.zhuangtai = zt
- if 'shenhe_zhuangtai' in request.data:
- szt = int(request.data.get('shenhe_zhuangtai'))
- if szt not in [0, 1]:
- return Response({'code': 1, 'msg': '无效的审核官状态'}, status=400)
- shenheguan.shenhe_zhuangtai = szt
- # 新增认证状态编辑
- if 'is_renzheng' in request.data:
- shenheguan.is_renzheng = bool(request.data.get('is_renzheng'))
- shenheguan.save()
- return Response({'code': 0, 'msg': '修改成功'})
-
- elif action == 'add_bankuai':
- bankuai_id = request.data.get('bankuai_id')
- if not bankuai_id:
- return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
- if not Bankuai.objects.filter(bankuai_id=bankuai_id).exists():
- return Response({'code': 1, 'msg': '板块不存在'}, status=400)
- if KaoheguanBankuai.objects.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists():
- return Response({'code': 1, 'msg': '已关联该板块'}, status=400)
- KaoheguanBankuai.objects.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id)
- return Response({'code': 0, 'msg': '板块关联成功'})
-
- elif action == 'remove_bankuai':
- bankuai_id = request.data.get('bankuai_id')
- if not bankuai_id:
- return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
- deleted, _ = KaoheguanBankuai.objects.filter(
- kaoheguan_id=yonghuid, bankuai_id=bankuai_id
- ).delete()
- if not deleted:
- return Response({'code': 1, 'msg': '未找到该板块关联'}, status=400)
- return Response({'code': 0, 'msg': '板块移除成功'})
-
- else:
- return Response({'code': 1, 'msg': '未知操作'}, status=400)
-
- except Exception as e:
- logger.exception("ShgxgsjView 接口错误")
- return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
-
-
-
-
-
-
-import secrets
-from dingdan.models import Dingdan, DingdanShangjia
-from dingdan.tongzhi_tasks import dingdan_guangbo
-
-from peizhi.models import ShangjiaLianjie
-
-class ZxkfghdsView(APIView):
- """
- 客服更换打手接口
- 路径:POST /houtai/zxkfghds
- 功能:
- 1. 将当前订单状态改为已退款(5),释放接单打手(若有)
- 2. 如需跨平台通知,则调用对方平台退款通知接口
- 3. 基于原订单信息生成全新订单(状态直接为 1,已替老板填写)
- 4. 生成新的派单链接,并标记为已使用
- 5. 新链接记录上一个链接ID和上一个订单ID
- 6. 【关键】不操作商家余额、退款次数、每日统计,因新订单是替换而非新增扣款
- 权限:需要'003aa'(跨平台管理)权限,与退款接口一致
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # ========== 1. 参数提取 ==========
- username = request.data.get('username', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- if not username or not dingdan_id:
- logger.warning("更换打手参数不完整")
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # ========== 2. 权限校验(与跨平台退款相同) ==========
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 返回错误响应
- if '003aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权处理订单'})
-
- current_user = request.user
-
- # ========== 3. 查询原订单及关联信息 ==========
- try:
- old_order = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id)
- except Dingdan.DoesNotExist:
- logger.warning(f"订单不存在: {dingdan_id}")
- return Response({'code': 404, 'msg': '订单不存在'})
-
- # 仅允许状态为 2(进行中)或 8(结算中)更换打手
- if old_order.zhuangtai not in [1,7,2, 8]:
- return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'})
-
- # 本接口仅处理商家发单(fadan_pingtai=2)
- if old_order.fadan_pingtai != 2:
- return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'})
-
- # 获取原链接(用于模板ID等)
- try:
- old_lianjie = ShangjiaLianjie.objects.get(dingdan_id=dingdan_id)
- except ShangjiaLianjie.DoesNotExist:
- logger.error(f"原链接记录不存在,订单: {dingdan_id}")
- return Response({'code': 500, 'msg': '链接数据异常'})
-
- # 商家信息(仅用于记录关联,不操作余额)
- try:
- shangjia_ext = old_order.shangjia_kuozhan
- shangjia_id = shangjia_ext.shangjia_id
- # 获取商家昵称用于新订单扩展表
- shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
- shangjia_nicheng = shangjia_user.shop_profile.nicheng or f"商家{shangjia_id}"
- except (ObjectDoesNotExist, UserShangjia.DoesNotExist):
- logger.error(f"商家信息缺失,订单: {dingdan_id}")
- return Response({'code': 500, 'msg': '商家数据异常'})
-
- # ========== 4. 判断是否需要跨平台退款通知 ==========
- is_cross = old_order.is_cross
- partner_club_id = old_order.partner_club_id
- partner_order_id = old_order.partner_order_id
- need_notify_partner = is_cross and partner_club_id and partner_order_id
-
- # ========== 5. 退款处理(仅打手侧,不碰商家) ==========
- with transaction.atomic():
- # 5.1 订单状态改为已退款
- old_order.zhuangtai = 5
- old_order.clkf = current_user.phone
- old_order.save()
-
- # 5.2 处理接单打手(若有)
- dashou_id = old_order.jiedan_dashou_id
- if dashou_id:
- try:
- dashou_user = UserMain.objects.get(yonghuid=dashou_id)
- dashou_profile = dashou_user.dashou_profile
- dashou_profile.tuikuanliang += 1 # 退款次数+1
- dashou_profile.zhuangtai = 1 # 设为空闲
- dashou_profile.save()
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"打手 {dashou_id} 不存在,跳过更新")
-
- # 5.3 更新退款记录表
- Tuikuanjilu.objects.update_or_create(
- dingdan_id=dingdan_id,
- defaults={
- 'dashouid': dashou_id or '',
- 'qingqiuid': '',
- 'chuliid': current_user.phone,
- 'liyou': '客服更换打手退款',
- 'sqzhuangtai': 1,
- 'jine': old_order.jine or 0,
- 'dashou_fencheng': old_order.dashou_fencheng or 0,
- 'jieshao': old_order.jieshao or '',
- 'beizhu': '更换打手自动退款',
- 'nicheng': old_order.nicheng or '',
- }
- )
-
- # 5.4 打手每日统计(退款)
- if dashou_id:
- try:
- update_dashou_daily_by_action(
- yonghuid=dashou_id,
- amount=old_order.dashou_fencheng,
- action=3 # 3=退款
- )
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
-
- # 5.5 客服处理统计
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- # 5.6 跨平台退款通知(需要时)
- if need_notify_partner:
- try:
- # 获取我方俱乐部ID
- my_club_config = ClubConfig.objects.filter(is_self=1).first()
- if not my_club_config:
- raise Exception("我方俱乐部配置不存在")
- self_club_id = my_club_config.club_id
- club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
- partner_domain = club_rel.partner_domain
-
- notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk'
- payload = {
- 'order_id': partner_order_id,
- 'club_id': self_club_id,
- 'status': 5,
- 'reason': '客服更换打手退款'
- }
-
- # 调用对方接口(必须成功,否则整个事务回滚)
- resp = requests.post(notify_url, json=payload, timeout=5)
- if resp.status_code == 200:
- resp_json = resp.json()
- if resp_json.get('code') != 0:
- transaction.set_rollback(True)
- logger.error(f"对方平台退款通知失败: {resp_json.get('msg')}")
- return Response({'code': 500, 'msg': f'对方平台通知失败: {resp_json.get("msg")}'})
- else:
- transaction.set_rollback(True)
- logger.error(f"对方平台接口HTTP错误: {resp.status_code}")
- return Response({'code': 500, 'msg': '通知对方平台失败'})
-
- # 更新跨平台扩展表(如果存在)
- from dingdan.models import CrossPlatformOrderData
- CrossPlatformOrderData.objects.update_or_create(
- dingdan_id=dingdan_id,
- defaults={
- 'partner_order_id': partner_order_id,
- 'partner_club_id': partner_club_id,
- 'partner_order_status': 5,
- }
- )
- except Exception as e:
- transaction.set_rollback(True)
- logger.error(f"通知对方平台异常: {e}")
- return Response({'code': 500, 'msg': f'通知对方平台失败: {str(e)}'})
-
- # 至此,原订单已退款,事务提交
-
- # ========== 6. 生成新订单和链接(不扣商家余额) ==========
- try:
- with transaction.atomic():
- # ---- 6.1 生成新的订单ID和Token ----
- def generate_dingdan_id():
- timestamp = int(time.time() * 1000)
- process_id = os.getpid() % 1000
- random_num = random.randint(1000, 9999)
- return f"SJ{timestamp}{process_id:03d}{random_num}"
-
- def generate_secure_token(dd_id, uid):
- timestamp = int(time.time() * 1_000_000)
- salt = secrets.token_hex(8)
- raw = f"{timestamp}:{dd_id}:{uid}:{salt}"
- token = hashlib.sha256(raw.encode()).hexdigest()[:24]
- if ShangjiaLianjie.objects.filter(lianjie_token=token).exists():
- return generate_secure_token(dd_id, uid)
- return token
-
- def generate_link_url(token):
- base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/')
- return f"{base}/order/{token}"
-
- new_dingdan_id = generate_dingdan_id()
- secure_token = generate_secure_token(new_dingdan_id, shangjia_id)
-
- # ---- 6.2 创建新订单(复制核心数据,状态直接为1) ----
- new_order = Dingdan.objects.create(
- dingdan_id=new_dingdan_id,
- zhuangtai=1, # 下单中
- fadan_pingtai=2,
- jine=old_order.jine,
- dashou_fencheng=old_order.dashou_fencheng,
- leixing_id=old_order.leixing_id,
- yaoqiuleixing=old_order.yaoqiuleixing,
- huiyuan_id=old_order.huiyuan_id,
- yongjin=old_order.yongjin,
- jieshao=old_order.jieshao,
- nicheng=old_order.nicheng, # 继承游戏昵称
- beizhu=old_order.beizhu,
- user1_id=old_order.user1_id, # 商家标识
- tupian=old_order.tupian,
- # 注意:不继承 zhiding_id、jiedan_dashou_id
- )
-
- # ---- 6.3 创建商家扩展表 ----
- DingdanShangjia.objects.create(
- dingdan=new_order,
- shangjia_id=shangjia_id,
- sjnicheng=shangjia_nicheng,
- )
-
- # ---- 6.4 生成新链接并关联旧链接信息 ----
- link_url = generate_link_url(secure_token)
- new_lianjie = ShangjiaLianjie.objects.create(
- yonghu_id=shangjia_id,
- leixing_id=old_lianjie.leixing_id,
- moban_id=old_lianjie.moban_id,
- lianjie=link_url,
- lianjie_token=secure_token,
- dingdan_id=new_dingdan_id,
- is_used=True, # 直接标记已使用
- youxiao_shijian=timezone.now() + timezone.timedelta(days=1),
- previous_lianjie_id=old_lianjie.id, # 记录旧链接ID
- previous_dingdan_id=old_order.dingdan_id, # 记录旧订单ID
- )
-
- # ---- 6.5 异步通知(广播新订单、跨平台同步) ----
- try:
- order_info = {
- 'dingdan_id': new_dingdan_id,
- 'game_type': self._get_game_type_name(new_order.leixing_id),
- 'amount': str(new_order.jine),
- 'order_desc': (new_order.jieshao or '')[:50],
- }
- dingdan_guangbo.delay(order_info)
- except Exception as e:
- logger.error(f"广播通知失败: {e}")
-
- # 跨平台同步(与客户填写接口保持一致)
- try:
- from dingdan.utils import sync_order_to_partners
- sync_order_to_partners(new_dingdan_id)
- except Exception as e:
- logger.error(f"跨平台同步失败: {e}")
-
- # 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单
-
- logger.info(f"更换打手成功:旧订单{dingdan_id}→退款,新订单{new_dingdan_id}已生成")
-
- return Response({
- 'code': 0,
- 'msg': '更换成功',
- 'data': {
- 'new_dingdan_id': new_dingdan_id,
- 'new_lianjie': link_url,
- }
- })
-
- except Exception as e:
- logger.error(f"生成新订单事务失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '生成新订单失败'})
-
- def _get_game_type_name(self, leixing_id):
- """根据类型ID获取游戏类型名称"""
- try:
- from shangpin.models import ShangpinLeixing
- gt = ShangpinLeixing.objects.filter(id=leixing_id).first()
- return gt.jieshao if gt else '游戏订单'
- except Exception:
- return '游戏订单'
-
-
-
-
-
-
-import logging
-
-from django.conf import settings
-from django.db.models import ObjectDoesNotExist
-from rest_framework import status
-from rest_framework.parsers import JSONParser
-from rest_framework.response import Response
-from rest_framework.views import APIView
-
-from dingdan.models import Dingdan
-
-logger = logging.getLogger('houtai')
-
-
-class KptxwztjbView(APIView):
- """
- 跨平台订单状态同步接口(对方平台调用)
- 请求:POST /houtai/kptxwztjb
- 参数:
- - token: 同步专用令牌(明文)
- - partner_order_id: 我方订单ID(在对方眼中是partner_order_id,实际查本库dingdan_id)
- - our_order_id: 对方订单ID(在本库中为partner_order_id)
- - club_id: 俱乐部ID(可选验证)
- 返回:
- code=0 成功,data.status 为订单状态
- 其他 code 为错误
- 注意:此接口仅用于查询对方订单状态,不做任何修改
- """
-
- authentication_classes = [] # 无需JWT等认证
- permission_classes = [] # 公开接口
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- token = request.data.get('token', '').strip()
- partner_order_id = request.data.get('partner_order_id', '').strip()
- our_order_id = request.data.get('our_order_id', '').strip()
- club_id = request.data.get('club_id', '').strip()
-
- if not all([token, partner_order_id, our_order_id]):
- logger.warning("跨平台状态同步参数缺失")
- return Response({'code': 400, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 验证同步令牌
- SYNC_TOKEN = getattr(settings, 'CROSS_PLATFORM_SYNC_TOKEN', None)
- if not SYNC_TOKEN or token != SYNC_TOKEN:
- logger.warning(f"无效的同步令牌: {token[:8]}...")
- return Response({'code': 403, 'msg': '无效令牌'}, status=status.HTTP_403_FORBIDDEN)
-
- # 3. 查询订单
- # partner_order_id 在对方视角是“我方订单ID”,在本库中应匹配 dingdan_id
- try:
- order = Dingdan.objects.get(dingdan_id=partner_order_id)
- except Dingdan.DoesNotExist:
- logger.warning(f"订单不存在, partner_order_id: {partner_order_id}")
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 验证订单关联关系:本库中的 partner_order_id 应等于传入的 our_order_id
- if order.partner_order_id != our_order_id:
- logger.warning(
- f"订单关联不匹配: partner_order_id={order.partner_order_id}, our_order_id={our_order_id}"
- )
- return Response({'code': 400, 'msg': '订单关联信息不匹配'}, status=status.HTTP_400_BAD_REQUEST)
-
-
-
- # 6. 返回订单状态
- status_code = order.zhuangtai
- logger.info(
- f"跨平台状态查询成功, dingdan_id={order.dingdan_id}, status={status_code}"
- )
- return Response({
- 'code': 0,
- 'msg': 'success',
- 'data': {
- 'dingdan_id': order.dingdan_id,
- 'status': status_code,
- }
+import hmac
+import threading
+import traceback
+import uuid
+import hashlib
+import xmltodict
+import time
+import logging
+import requests
+import os
+import secrets
+import random
+import string
+from calendar import monthrange
+from decimal import Decimal
+from datetime import date, datetime, timedelta
+from django.utils import timezone
+from django.conf import settings
+from django.core.exceptions import ObjectDoesNotExist
+from django.core.paginator import Paginator
+from django.db import models, transaction, IntegrityError
+from django.db.models import Q, Count, Sum, F, Exists, OuterRef
+from django.db.models.functions import TruncDate, TruncMonth, TruncYear
+from django.contrib.auth.hashers import make_password
+from django.views.decorators.csrf import csrf_exempt
+from django.utils.decorators import method_decorator
+
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework import status
+from rest_framework.permissions import IsAuthenticated, AllowAny
+from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
+
+# 工具类导入
+from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
+from backend.utils import (
+ update_dashou_daily_by_action,
+ update_shangjia_daily
+)
+from orders.utils import update_daily_dispatch_stat, update_daily_payout, sync_order_to_partners
+from shop.utils import update_dianpu_daily_stat
+from products.utils import update_shangpin_daily_stat
+from orders.notice_tasks import dingdan_guangbo
+from .utils import verify_kefu_permission
+
+# models 集中导入
+## backend
+from backend.models import Role, Permission, RolePermission, UserRole, TixianRiTongji
+
+## users
+from users.models import (
+ UserMain, UserGuanshi, UserBoss, UserZuzhang,
+ UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
+)
+
+## orders
+from orders.models import (
+ Lilubiao, Dashoutupian, Chufajilu, PreSettlement,
+ OrderDashouHistory, Dingdan, Tuikuanjilu, CrossPlatformOrderData,
+ DingdanShangjia, Fadan, FadanShensuTupian, FadanFenhong
+)
+
+## shop
+from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
+
+## products
+from products.models import (
+ Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
+ DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
+)
+
+## config
+from config.models import (
+ WithdrawConfig, ShangjiaLianjie, AccountPermission, ClubConfig,
+ Club, DailyDispatchStat, OrderTypeMapping, TixianQuotaDefault,
+ DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
+)
+
+## rank
+from rank.models import (
+ KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
+ YonghuChenghao, ShenheJilu
+)
+
+# 序列化器
+from .serializers import PopupPageSerializer, PopupConfigSerializer, PopupImageSerializer
+
+# 全局常量、日志对象
+logger = logging.getLogger('houtai')
+SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
+
+
+
+class GetClubConfigView(APIView):
+ """
+ 获取对方俱乐部配置列表(用于跨平台选择)
+ 权限要求:客服身份 + 权限代码1且开启
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ """
+ 处理 POST 请求,返回对方俱乐部配置数据(is_self=0)
+ """
+ try:
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '003aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
+
+ # 5. 查询所有对方俱乐部(is_self=0),只取必要字段
+ club_configs = ClubConfig.objects.filter(
+ is_self=0 # 只返回对方俱乐部
+ ).only(
+ 'club_id',
+ 'club_nickname',
+ 'club_avatar',
+ 'storage_bucket_domain' # 如果前端需要存储桶域名,一并返回
+ )
+
+ # 构造返回数据列表
+ data_list = []
+ for club in club_configs:
+ data_list.append({
+ 'club_id': club.club_id,
+ 'club_nickname': club.club_nickname or club.club_id, # 若昵称为空,用ID代替
+ 'club_avatar': club.club_avatar or '', # 可能为空
+ 'storage_bucket_domain': club.storage_bucket_domain or '' # 可选
+ })
+
+ return Response({
+ 'code': 0,
+ 'msg': 'success',
+ 'data': data_list
+ })
+
+ except Exception as e:
+ logger.error(f"获取俱乐部配置失败: {str(e)}")
+ return Response({
+ 'code': 500,
+ 'msg': '服务器内部错误'
+ })
+
+
+
+
+
+class GetCrossOrderListView(APIView):
+ """
+ 获取跨平台订单列表(客服后台)
+ 权限要求:客服身份 + 权限代码1且开启
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ """
+ 处理 POST 请求,返回分页订单数据及统计信息
+ """
+ try:
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '003aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看俱乐部订单'})
+
+ # 5. 获取请求参数
+ data = request.data
+
+
+
+
+ # 分页参数
+ page = int(data.get('page', 1))
+ page_size = int(data.get('pageSize', 20))
+
+ # 查询参数
+ leixing = data.get('leixing') # 订单类型ID
+ zhuangtai = data.get('zhuangtai') # 状态字符串,如 "1,7"
+ club_id = data.get('club_id') # 对方俱乐部ID(精确)
+ direction = data.get('direction') # send/receive/all
+ dingdan_id = data.get('dingdan_id') # 订单ID模糊搜索
+ club_id_search = data.get('club_id_search') # 对方俱乐部ID模糊搜索
+ date = data.get('date') # 日期 YYYY-MM-DD
+ dashou_id = data.get('dashou_id') # 打手ID(接单打手ID)
+ clkf = data.get('clkf') # 处理客服
+
+ # 6. 构建基础查询(跨平台订单)
+ base_q = Q(is_cross=1)
+
+ # 添加筛选条件(非空且非全部)
+ if leixing and leixing != '':
+ base_q &= Q(leixing_id=leixing)
+
+ if zhuangtai and zhuangtai != 'all':
+ # 解析逗号分隔的状态码
+ status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip().isdigit()]
+ if status_list:
+ base_q &= Q(zhuangtai__in=status_list)
+
+ if club_id and club_id != '':
+ base_q &= Q(partner_club_id=club_id)
+
+ # 方向映射
+ if direction == 'send':
+ base_q &= Q(dispatch_type=1)
+ elif direction == 'receive':
+ base_q &= Q(dispatch_type=2)
+ # direction == 'all' 时不加方向条件
+
+ if dingdan_id:
+ base_q &= Q(dingdan_id__icontains=dingdan_id)
+
+ if club_id_search:
+ base_q &= Q(partner_club_id__icontains=club_id_search)
+
+ if date:
+ # 按日期精确过滤(假设 create_time 是带时区的 DateTimeField)
+ base_q &= Q(create_time__date=date)
+
+ if dashou_id:
+ base_q &= Q(jiedan_dashou_id=dashou_id)
+
+ if clkf:
+ base_q &= Q(clkf__icontains=clkf)
+
+ # 7. 统计各状态数量(基于相同过滤条件)
+ # 获取所有符合条件的订单的状态分组统计
+ status_counts = Dingdan.objects.filter(base_q).values('zhuangtai').annotate(count=Count('id')).order_by()
+
+ # 转换为字典 {状态码: 数量}
+ status_map = {item['zhuangtai']: item['count'] for item in status_counts}
+
+ # 定义状态列表(与前端一致)
+ status_list_map = {
+ 'all': 0, # 稍后填充总数量
+ '1,7': 0,
+ '2': 0,
+ '8': 0,
+ '3': 0,
+ '4': 0,
+ '5': 0,
+ '6': 0,
+ }
+ # 填充总数
+ status_list_map['all'] = sum(status_map.values())
+ # 填充单个状态
+ for code, cnt in status_map.items():
+ if code == 1 or code == 7:
+ status_list_map['1,7'] += cnt
+ elif code in status_list_map:
+ status_list_map[str(code)] = cnt
+ # 其他状态暂不处理
+
+ # 构建 stats 统计
+ total_orders = status_list_map['all']
+ completed_orders = status_map.get(3, 0)
+ refund_orders = status_map.get(5, 0)
+ pending_orders = status_map.get(8, 0) + status_map.get(4, 0)
+
+ stats = {
+ 'totalOrders': total_orders,
+ 'completedOrders': completed_orders,
+ 'refundOrders': refund_orders,
+ 'pendingOrders': pending_orders,
+ }
+
+ # 8. 分页获取订单列表
+ # 按创建时间倒序,保证分页稳定
+ orders_qs = Dingdan.objects.filter(base_q).order_by('-create_time')
+
+ # 使用 Paginator 分页
+ paginator = Paginator(orders_qs, page_size)
+ current_page = paginator.get_page(page)
+ order_list = current_page.object_list
+
+ # 只返回前端需要的字段,减少数据传输
+ data_list = []
+ for order in order_list:
+ data_list.append({
+ 'dingdan_id': order.dingdan_id,
+ 'nicheng': order.nicheng or '',
+ 'jieshao': order.jieshao or '',
+ 'jiage': float(order.jine) if order.jine else 0,
+ 'zhuangtai': order.zhuangtai,
+ })
+
+ # 9. 返回结果
+ return Response({
+ 'code': 0,
+ 'msg': 'success',
+ 'data': {
+ 'list': data_list,
+ 'total': total_orders,
+ 'statusCounts': status_list_map,
+ 'stats': stats,
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"获取跨平台订单列表失败: {str(e)}")
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+
+
+
+
+class GetCrossOrderDetailView(APIView):
+ """
+ 客服获取跨平台订单详情接口
+ 请求:POST /houtai/hqkptddxq
+ 参数:{"username": "客服手机号", "dingdan_id": "订单ID"}
+ 认证:JWT,权限码1
+ 返回:code=0 + 订单详情数据(包含俱乐部信息、打手图片、处罚记录等)
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ start_time = time.time()
+
+ # 1. 获取参数
+ username = request.data.get('username', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ if not username or not dingdan_id:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ try:
+ # 2. 身份验证
+ current_user = request.user
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '003aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看俱乐部订单'})
+
+ # 4. 查询订单
+ dingdan_obj = Dingdan.objects.select_related(
+ 'pingtai_kuozhan',
+ 'shangjia_kuozhan'
+ ).get(dingdan_id=dingdan_id)
+
+ # 5. 构建基础响应数据
+ response_data = {
+ 'dingdan_id': dingdan_obj.dingdan_id or '',
+ 'zhuangtai': dingdan_obj.zhuangtai or 0,
+ 'fadanpingtai': dingdan_obj.fadan_pingtai or 0,
+ 'dispatch_type': dingdan_obj.dispatch_type or 0, # 1=我方派单, 2=对方派单
+ 'is_cross': dingdan_obj.is_cross or 0,
+ 'partner_order_id': dingdan_obj.partner_order_id or '',
+ 'partner_club_id': dingdan_obj.partner_club_id or '',
+ 'jine': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00,
+ 'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00,
+ 'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '',
+ 'dashou_liuyan': dingdan_obj.dashou_liuyan or '',
+ 'zhiding_id': dingdan_obj.zhiding_id or '',
+ 'shangpin_id': dingdan_obj.shangpin_id or '',
+ 'leixing': dingdan_obj.leixing_id or 0,
+ 'tupian_url': dingdan_obj.tupian or '',
+ 'jieshao': dingdan_obj.jieshao or '',
+ 'beizhu': dingdan_obj.beizhu or '',
+ 'tkly': dingdan_obj.tkly or '',
+ 'nicheng': dingdan_obj.nicheng or '',
+ 'clkf': dingdan_obj.clkf or '',
+ 'chuangjianshijian': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.create_time else '',
+ 'genggaishijian': dingdan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.update_time else '',
+ }
+
+ # 6. 扩展表数据
+ if dingdan_obj.fadan_pingtai == 1:
+ if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan:
+ response_data['pingtai_kuozhan'] = {
+ 'laoban_id': dingdan_obj.pingtai_kuozhan.laoban_id or '',
+ 'laoban_pingjia': dingdan_obj.pingtai_kuozhan.laoban_pingjia or '',
+ }
+ else:
+ response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''}
+ elif dingdan_obj.fadan_pingtai == 2:
+ if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan:
+ shangjia = dingdan_obj.shangjia_kuozhan
+ response_data['shangjia_kuozhan'] = {
+ 'shangjia_id': shangjia.shangjia_id or '',
+ 'sjnicheng': shangjia.sjnicheng or '',
+ 'shangjia_pingjia': shangjia.shangjia_pingjia or '',
+ 'cfliyou': shangjia.cfliyou or '',
+ 'sqzhuangtai': shangjia.sqzhuangtai if shangjia.sqzhuangtai is not None else 0,
+ 'bhliyou': shangjia.bhliyou or '',
+ }
+ else:
+ response_data['shangjia_kuozhan'] = {
+ 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '',
+ 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': ''
+ }
+
+ # ========== 核心逻辑:判断派单/接单关系 ==========
+ dispatch_type = dingdan_obj.dispatch_type # 1=我方派单, 2=对方派单
+ is_cross = dingdan_obj.is_cross
+ partner_club_id = dingdan_obj.partner_club_id
+ partner_order_id = dingdan_obj.partner_order_id
+
+ # 获取我方俱乐部配置(用于本地存储桶域名)
+ my_club_config = ClubConfig.objects.filter(is_self=1).first()
+ my_storage_domain = my_club_config.storage_bucket_domain if my_club_config else ''
+
+ # 初始化字段
+ response_data['club_info'] = None # 对方俱乐部信息
+ response_data['partner_images'] = [] # 打手提交图片(对方平台打手或我方打手)
+ response_data['punishment_info'] = {} # 处罚信息
+ response_data['is_punished'] = False # 是否已处罚(用于前端禁用按钮)
+ response_data['receive_side'] = '' # 接单方:'self' 我方接单,'partner' 对方接单
+ response_data['dispatch_side'] = '' # 派单方:'self' 我方派单,'partner' 对方派单
+
+ # 设置派单方
+ if dispatch_type == 1:
+ response_data['dispatch_side'] = 'self'
+ elif dispatch_type == 2:
+ response_data['dispatch_side'] = 'partner'
+ else:
+ # 兼容旧数据:根据 partner_club_id 或 partner_order_id 推断
+ if partner_club_id or partner_order_id:
+ response_data['dispatch_side'] = 'partner'
+ else:
+ response_data['dispatch_side'] = 'self'
+
+ # 情况1:对方派单(对方平台发单,我方打手接单)
+ if response_data['dispatch_side'] == 'partner':
+ # 接单方为我方
+ response_data['receive_side'] = 'self'
+
+ # 获取对方俱乐部信息(用于展示)
+ if partner_club_id:
+ partner_config = ClubConfig.objects.filter(club_id=partner_club_id, is_self=0).first()
+ if partner_config:
+ response_data['club_info'] = {
+ 'club_id': partner_config.club_id,
+ 'club_nickname': partner_config.club_nickname or partner_config.club_id,
+ 'club_avatar': partner_config.club_avatar or '',
+ }
+ else:
+ response_data['club_info'] = {'club_id': partner_club_id, 'club_nickname': '', 'club_avatar': ''}
+ else:
+ response_data['club_info'] = None
+
+ # 查询我方打手提交的图片(本地)
+ dashou_images = Dashoutupian.objects.filter(
+ dingdan_id=dingdan_id,
+ dashou=dingdan_obj.jiedan_dashou_id
+ ).values_list('tupian', flat=True)
+ images = []
+ for rel_path in dashou_images:
+ if rel_path:
+ if rel_path.startswith('http'):
+ images.append(rel_path)
+ else:
+ images.append(my_storage_domain + rel_path)
+ response_data['partner_images'] = images
+
+ # 查询我方打手处罚记录
+ punish_records = Chufajilu.objects.filter(
+ dingdan_id=dingdan_id,
+ dashouid=dingdan_obj.jiedan_dashou_id
+ ).order_by('-create_time')
+ if punish_records.exists():
+ latest = punish_records.first()
+ response_data['punishment_info'] = {
+ 'cfliyou': latest.cfliyou or '',
+ 'sqzhuangtai': latest.sqzhuangtai or 0,
+ 'bhliyou': latest.bhliyou or '',
+ 'ssliyou': latest.ssliyou or '',
+ 'jifen': latest.jifen or 0,
+ }
+ response_data['is_punished'] = True
+ else:
+ response_data['punishment_info'] = {}
+ response_data['is_punished'] = False
+
+ # 情况2:我方派单(我方平台发单)
+ else:
+ response_data['dispatch_side'] = 'self'
+ # 根据是否有对方订单ID/俱乐部ID判断接单方
+ if partner_club_id or partner_order_id:
+ # 有对方信息,说明订单派给了对方平台,对方接单
+ response_data['receive_side'] = 'partner'
+
+ # 获取对方俱乐部信息
+ if partner_club_id:
+ partner_config = ClubConfig.objects.filter(club_id=partner_club_id, is_self=0).first()
+ if partner_config:
+ response_data['club_info'] = {
+ 'club_id': partner_config.club_id,
+ 'club_nickname': partner_config.club_nickname or partner_config.club_id,
+ 'club_avatar': partner_config.club_avatar or '',
+ }
+ else:
+ response_data['club_info'] = {'club_id': partner_club_id, 'club_nickname': '', 'club_avatar': ''}
+
+ # 需要调用对方接口获取图片和处罚记录
+ if partner_club_id and partner_order_id:
+ # 获取对方服务器域名
+ if my_club_config:
+ self_club_id = my_club_config.club_id
+ else:
+ logger.error(f"无法获取本俱乐部ID,订单ID: {dingdan_id}")
+ return Response({'code': 500, 'msg': '系统配置错误,请联系管理员'})
+
+ try:
+ club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
+ partner_domain = club_rel.partner_domain
+ except ObjectDoesNotExist:
+ logger.error(f"未找到合作关系: {self_club_id} <-> {partner_club_id}")
+ return Response({'code': 500, 'msg': '未找到对方平台配置'})
+
+ # 调用对方接口,最多重试3次
+ url = partner_domain.rstrip('/') + '/houtai/kpthqtp'
+ payload = {
+ 'order_id': partner_order_id,
+ 'club_id': self_club_id,
+ }
+ timeout = 3
+ retries = 3
+ success = False
+ for attempt in range(retries):
+ try:
+ resp = requests.post(url, json=payload, timeout=timeout)
+ if resp.status_code == 200:
+ resp_json = resp.json()
+ if resp_json.get('code') == 0:
+ data = resp_json.get('data', {})
+ response_data['partner_images'] = data.get('images', [])
+ response_data['punishment_info'] = data.get('punishment', {})
+ response_data['is_punished'] = bool(response_data['punishment_info'].get('cfliyou'))
+ success = True
+ break
+ else:
+ logger.warning(f"对方接口返回错误 (尝试{attempt+1}): {resp_json.get('msg')}")
+ else:
+ logger.warning(f"对方接口HTTP错误 (尝试{attempt+1}): {resp.status_code}")
+ except requests.exceptions.Timeout:
+ logger.warning(f"请求对方接口超时 (尝试{attempt+1}): {url}")
+ except Exception as e:
+ logger.warning(f"请求对方接口异常 (尝试{attempt+1}): {str(e)}")
+ if not success:
+ # 三次失败,返回部分数据,但提示前端对方数据获取失败
+ response_data['partner_images'] = []
+ response_data['punishment_info'] = {}
+ response_data['is_punished'] = False
+ response_data['partner_data_error'] = '获取对方平台数据失败,请稍后重试'
+ else:
+ # 对方信息不完整,无法获取数据
+ response_data['partner_images'] = []
+ response_data['punishment_info'] = {}
+ response_data['is_punished'] = False
+ response_data['partner_data_error'] = '对方订单信息不完整'
+ else:
+ # 无对方信息,说明我方派单且我方接单
+ response_data['receive_side'] = 'self'
+ # 我方打手提交图片(本地)
+ dashou_images = Dashoutupian.objects.filter(
+ dingdan_id=dingdan_id,
+ dashou=dingdan_obj.jiedan_dashou_id
+ ).values_list('tupian', flat=True)
+ images = []
+ for rel_path in dashou_images:
+ if rel_path:
+ if rel_path.startswith('http'):
+ images.append(rel_path)
+ else:
+ images.append(my_storage_domain + rel_path)
+ response_data['partner_images'] = images
+
+ # 处罚记录
+ punish_records = Chufajilu.objects.filter(
+ dingdan_id=dingdan_id,
+ dashouid=dingdan_obj.jiedan_dashou_id
+ ).order_by('-create_time')
+ if punish_records.exists():
+ latest = punish_records.first()
+ response_data['punishment_info'] = {
+ 'cfliyou': latest.cfliyou or '',
+ 'sqzhuangtai': latest.sqzhuangtai or 0,
+ 'bhliyou': latest.bhliyou or '',
+ 'ssliyou': latest.ssliyou or '',
+ 'jifen': latest.jifen or 0,
+ }
+ response_data['is_punished'] = True
+ else:
+ response_data['punishment_info'] = {}
+ response_data['is_punished'] = False
+
+ # 如果是我方派单且我方接单,对方俱乐部信息为空
+ response_data['club_info'] = None
+
+ # 7. 查询退款理由
+ try:
+ tuikuan_record = Tuikuanjilu.objects.filter(
+ dingdan_id=dingdan_id
+ ).order_by('-create_time').first()
+ response_data['tuikuan_liyou'] = tuikuan_record.liyou if tuikuan_record else ''
+ except Exception as e:
+ logger.warning(f"查询退款记录失败: {str(e)}")
+ response_data['tuikuan_liyou'] = ''
+
+ # ========== 新增:查询更换打手生成的新订单信息 ==========
+ try:
+ new_lianjie = ShangjiaLianjie.objects.filter(
+ previous_dingdan_id=dingdan_id
+ ).order_by('-create_time').first()
+ if new_lianjie:
+ response_data['new_dingdan_id'] = new_lianjie.dingdan_id or ''
+ response_data['new_lianjie'] = new_lianjie.lianjie or ''
+ else:
+ response_data['new_dingdan_id'] = ''
+ response_data['new_lianjie'] = ''
+ except Exception as e:
+ logger.warning(f"查询新订单链接失败: {str(e)}")
+ response_data['new_dingdan_id'] = ''
+ response_data['new_lianjie'] = ''
+ # =====================================================
+
+ elapsed = time.time() - start_time
+ logger.info(f"获取跨平台订单详情接口耗时: {elapsed:.3f}s, 订单ID: {dingdan_id}")
+ return Response({'code': 0, 'data': response_data})
+
+ except Dingdan.DoesNotExist:
+ logger.warning(f"订单不存在: {dingdan_id}")
+ return Response({'code': 404, 'msg': '订单不存在'})
+ except Exception as e:
+ logger.error(f"获取跨平台订单详情接口异常: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+class CrossPlatformRefundView(APIView):
+ """
+ 跨平台订单退款接口(同时支持本地订单和跨平台订单)
+ 请求:POST /houtai/kptk
+ 参数:{
+ "username": "客服账号ID",
+ "dingdan_id": "订单ID",
+ "tuikuan_liyou": "退款理由(可选)"
+ }
+ 认证:JWT
+ 返回:code=0 成功,其他失败
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ username = request.data.get('username', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
+
+ if not username or not dingdan_id:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 身份验证
+ current_user = request.user
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '003aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权处理订单'})
+
+ # 4. 查询订单(带扩展表)
+ try:
+ order = Dingdan.objects.select_related(
+ 'pingtai_kuozhan',
+ 'shangjia_kuozhan'
+ ).get(dingdan_id=dingdan_id)
+ except Dingdan.DoesNotExist:
+ logger.warning(f"订单不存在: {dingdan_id}")
+ return Response({'code': 404, 'msg': '订单不存在'})
+
+ # 5. 订单状态校验(只有 1,7,2,8,4 可退款)
+ allowed_statuses = [1, 7, 2, 8, 4]
+ if order.zhuangtai not in allowed_statuses:
+ logger.warning(f"订单状态不允许退款: {order.zhuangtai}")
+ return Response({'code': 400, 'msg': '订单状态不允许退款'})
+
+ # 6. 根据派单方向分流
+ dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单
+
+ # ---------- 情况一:对方派单(我方打手接对方的单)----------
+ if dispatch_type == 2:
+ # 对方派单,资金在对方平台,我方只处理打手状态和订单状态
+ with transaction.atomic():
+ # 更新订单状态为已退款
+ order.zhuangtai = 5
+ order.clkf = current_user.phone
+ if tuikuan_liyou:
+ order.tkly = tuikuan_liyou
+ order.save()
+
+ # 更新打手数据(接单打手)
+ dashou_id = order.jiedan_dashou_id
+ if dashou_id:
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ dashou_profile.tuikuanliang += 1
+ dashou_profile.zhuangtai = 1 # 设为空闲
+ dashou_profile.save()
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"对方派单退款:打手 {dashou_id} 不存在,跳过更新")
+
+ # 更新退款记录(记录操作)
+ try:
+ refund_record, created = Tuikuanjilu.objects.get_or_create(
+ dingdan_id=dingdan_id,
+ defaults={
+ 'dashouid': dashou_id or '',
+ 'qingqiuid': '',
+ 'chuliid': current_user.phone,
+ 'liyou': tuikuan_liyou or '客服退款(对方派单)',
+ 'sqzhuangtai': 1,
+ 'jine': order.jine or 0,
+ 'dashou_fencheng': order.dashou_fencheng or 0,
+ 'jieshao': order.jieshao or '',
+ 'beizhu': '对方派单退款',
+ 'nicheng': order.nicheng or '',
+ }
+ )
+ if not created:
+ refund_record.sqzhuangtai = 1
+ refund_record.chuliid = current_user.phone
+ if tuikuan_liyou:
+ refund_record.liyou = tuikuan_liyou
+ refund_record.save()
+ except Exception as e:
+ logger.error(f"更新退款记录失败: {str(e)}")
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.jiedan_dashou_id,
+ amount=order.dashou_fencheng,
+ action=3 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+
+ # ✅ 新方法(行为驱动,更安全)
+ shangjia_ext = order.shangjia_kuozhan
+ shangjia_id = shangjia_ext.shangjia_id
+ update_shangjia_daily(
+ yonghuid=shangjia_id,
+ amount=Decimal(str(order.jine)),
+ action=3 # 3 = 退款
+ )
+
+ # 更新客服统计
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ logger.info(f"对方派单退款成功,订单 {dingdan_id}")
+ return Response({'code': 0, 'msg': '退款成功'})
+
+ # ---------- 情况二:我方派单(订单由我方发出)----------
+ # 需要进一步判断是本地订单还是跨平台订单
+ is_cross = order.is_cross
+ partner_club_id = order.partner_club_id
+ partner_order_id = order.partner_order_id
+
+ # 子情况2.1:本地订单(非跨平台)
+ if not is_cross or not partner_club_id:
+ # 按原有逻辑处理:平台订单或商家订单
+ if order.fadan_pingtai == 1:
+ # 平台订单退款(调用微信支付)
+ return self._refund_platform_order(order, current_user, tuikuan_liyou, kefu)
+ elif order.fadan_pingtai == 2:
+ # 商家订单退款(直接退余额)
+ return self._refund_merchant_order(order, current_user, tuikuan_liyou, kefu)
+ else:
+ return Response({'code': 400, 'msg': '未知的发单平台类型'})
+
+ # 子情况2.2:跨平台订单(我方派单给对方)
+ else:
+ # 必须先执行本地资金退款(因为钱在我方平台)
+ # 根据发单平台类型执行退款
+ local_refund_success = False
+ try:
+ with transaction.atomic():
+ if order.fadan_pingtai == 1:
+ # 平台订单:调用微信退款
+ refund_result = self._call_wechat_refund(order)
+ if refund_result['code'] != 0:
+ raise Exception(refund_result['msg'])
+ # 微信退款成功,更新订单状态
+ order.zhuangtai = 5
+ order.clkf = current_user.phone
+ if tuikuan_liyou:
+ order.tkly = tuikuan_liyou
+ order.save()
+ local_refund_success = True
+ elif order.fadan_pingtai == 2:
+ # 商家订单:退余额给商家
+ self._refund_to_merchant(order, current_user.phone, tuikuan_liyou)
+ order.zhuangtai = 5
+ order.clkf = current_user.phone
+ if tuikuan_liyou:
+ order.tkly = tuikuan_liyou
+ order.save()
+ local_refund_success = True
+ else:
+ return Response({'code': 400, 'msg': '未知的发单平台类型'})
+
+ # 更新打手数据(接单打手)
+ dashou_id = order.jiedan_dashou_id
+ if dashou_id:
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ dashou_profile.tuikuanliang += 1
+ dashou_profile.zhuangtai = 1
+ dashou_profile.save()
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"跨平台退款:打手 {dashou_id} 不存在,跳过更新")
+
+ # 更新退款记录
+ self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou)
+
+ # 更新客服统计
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ except Exception as e:
+ logger.error(f"跨平台订单本地退款失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': f'本地退款失败: {str(e)}'})
+
+ # 本地退款成功后,通知对方平台
+ if local_refund_success:
+ # 查询对方平台域名
+ try:
+ # 获取我方俱乐部ID
+ my_club_config = ClubConfig.objects.filter(is_self=1).first()
+ if not my_club_config:
+ logger.error("未配置我方俱乐部信息")
+ # 本地已成功,仅记录错误,不影响返回
+ else:
+ self_club_id = my_club_config.club_id
+ club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
+ partner_domain = club_rel.partner_domain
+
+ # 调用对方退款通知接口
+ notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk'
+ payload = {
+ 'order_id': partner_order_id, # 对方订单ID
+ 'club_id': self_club_id, # 我方俱乐部ID
+ 'status': 5, # 通知对方我方已退款
+ 'reason': tuikuan_liyou
+ }
+ # 设置超时3秒,不阻塞主流程
+ try:
+ resp = requests.post(notify_url, json=payload, timeout=3)
+ if resp.status_code == 200:
+ resp_json = resp.json()
+ if resp_json.get('code') == 0:
+ # 更新跨平台订单扩展表
+ cross_data, _ = CrossPlatformOrderData.objects.get_or_create(
+ dingdan_id=dingdan_id,
+ defaults={
+ 'partner_order_id': partner_order_id,
+ 'partner_club_id': partner_club_id,
+ 'partner_order_status': resp_json.get('data', {}).get('partner_status', 0),
+ 'partner_dashou_id': resp_json.get('data', {}).get('partner_dashou_id', ''),
+ 'partner_amount': order.jine or 0,
+ }
+ )
+ if not _:
+ cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0)
+ cross_data.save()
+ else:
+ logger.warning(f"对方平台返回错误: {resp_json.get('msg')}")
+ else:
+ logger.warning(f"对方平台接口HTTP错误: {resp.status_code}")
+ except requests.exceptions.Timeout:
+ logger.error(f"通知对方平台超时: {notify_url}")
+ except Exception as e:
+ logger.error(f"通知对方平台异常: {str(e)}")
+ except Exception as e:
+ logger.error(f"查询对方平台配置失败: {str(e)}")
+
+ return Response({'code': 0, 'msg': '退款成功'})
+
+ # ========== 辅助方法 ==========
+ def _refund_platform_order(self, order, current_user, tuikuan_liyou, kefu):
+ """平台订单退款(调用微信支付)"""
+ # 调用微信退款
+ refund_result = self._call_wechat_refund(order)
+ if refund_result['code'] != 0:
+ return Response({'code': 400, 'msg': refund_result['msg']})
+
+ with transaction.atomic():
+ order.zhuangtai = 5
+ order.clkf = current_user.phone
+ if tuikuan_liyou:
+ order.tkly = tuikuan_liyou
+ order.save()
+
+ # 更新打手
+ dashou_id = order.jiedan_dashou_id
+ if dashou_id:
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ dashou_profile.tuikuanliang += 1
+ dashou_profile.zhuangtai = 1
+ dashou_profile.save()
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"平台退款:打手 {dashou_id} 不存在,跳过更新")
+
+ # 更新退款记录
+ self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou)
+
+ is_cross = order.is_cross
+ partner_club_id = order.partner_club_id
+ partner_order_id = order.partner_order_id
+
+ if not is_cross or not partner_order_id:
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.jiedan_dashou_id,
+ amount=order.dashou_fencheng,
+ action=3 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+ else:
+ pass
+
+ # 客服统计
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ logger.info(f"平台订单退款成功: {order.dingdan_id}")
+ return Response({'code': 0, 'msg': '退款成功'})
+
+ def _refund_merchant_order(self, order, current_user, tuikuan_liyou, kefu):
+ """商家订单退款(退余额给商家)"""
+ # 获取商家ID
+ try:
+ shangjia_ext = order.shangjia_kuozhan
+ shangjia_id = shangjia_ext.shangjia_id
+ except ObjectDoesNotExist:
+ return Response({'code': 400, 'msg': '订单商家信息不存在'})
+
+ with transaction.atomic():
+ # 订单状态改已退款
+ order.zhuangtai = 5
+ order.clkf = current_user.phone
+ if tuikuan_liyou:
+ order.tkly = tuikuan_liyou
+ order.save()
+
+ # 退余额给商家
+ try:
+ shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
+ shangjia_profile = shangjia_user.shop_profile
+ shangjia_profile.tuikuan += 1
+ shangjia_profile.yue += order.jine or 0
+ shangjia_profile.save()
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ logger.error(f"商家退款:商家 {shangjia_id} 不存在,无法退余额")
+ raise Exception("商家账户不存在")
+
+ # 更新打手
+ dashou_id = order.jiedan_dashou_id
+ if dashou_id:
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ dashou_profile.tuikuanliang += 1
+ dashou_profile.zhuangtai = 1
+ dashou_profile.save()
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"商家退款:打手 {dashou_id} 不存在,跳过更新")
+
+ # 更新退款记录
+ self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou)
+
+
+ is_cross = order.is_cross
+ partner_club_id = order.partner_club_id
+ partner_order_id = order.partner_order_id
+
+ if not is_cross or not partner_order_id:
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.jiedan_dashou_id,
+ amount=order.dashou_fencheng,
+ action=3 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+ else:
+ pass
+
+ # ✅ 新方法(行为驱动,更安全)
+ update_shangjia_daily(
+ yonghuid=shangjia_id,
+ amount=Decimal(str(order.jine)),
+ action=3 # 3 = 退款
+ )
+
+ # 客服统计
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ logger.info(f"商家订单退款成功: {order.dingdan_id}")
+ return Response({'code': 0, 'msg': '退款成功'})
+
+ def _call_wechat_refund(self, order):
+ """调用微信支付退款接口,返回 {'code':0, 'msg':'success'} 或错误信息"""
+ # 获取微信支付配置
+ appid = getattr(settings, 'WEIXIN_APPID', '')
+ mch_id = getattr(settings, 'WEIXIN_MCHID', '')
+ key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '')
+ cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '')
+ key_path = getattr(settings, 'WEIXIN_KEY_PATH', '')
+
+ if not all([appid, mch_id, key, cert_path, key_path]):
+ missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v]
+ logger.error(f"微信支付配置缺失: {missing}")
+ return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'}
+
+ # 生成退款单号
+ out_refund_no = self._generate_refund_no(order.dingdan_id)
+ total_fee = int(float(order.jine or 0) * 100)
+ refund_fee = total_fee
+ refund_desc = '客服退款'
+
+ params = {
+ 'appid': appid,
+ 'mch_id': mch_id,
+ 'nonce_str': self._generate_nonce_str(),
+ 'out_refund_no': out_refund_no,
+ 'total_fee': total_fee,
+ 'refund_fee': refund_fee,
+ 'refund_desc': refund_desc,
+ }
+ # 优先使用微信交易号
+ if order.wechat_transaction_id:
+ params['transaction_id'] = order.wechat_transaction_id
+ else:
+ params['out_trade_no'] = order.dingdan_id
+
+ # 签名
+ sorted_keys = sorted(params.keys())
+ stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys])
+ stringSignTemp = f"{stringA}&key={key}"
+ sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
+ params['sign'] = sign
+
+ xml_data = self._dict_to_xml(params)
+ url = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
+
+ try:
+ response = requests.post(url, data=xml_data, cert=(cert_path, key_path), timeout=10)
+ result = xmltodict.parse(response.content)['xml']
+ logger.info(f"微信退款响应: {result}")
+ except Exception as e:
+ logger.error(f"微信退款请求异常: {str(e)}", exc_info=True)
+ return {'code': 500, 'msg': '微信退款请求异常'}
+
+ if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
+ return {'code': 0, 'msg': 'success'}
+ else:
+ err_msg = result.get('err_code_des', result.get('return_msg', '未知错误'))
+ logger.warning(f"微信退款失败: {err_msg}")
+ return {'code': 400, 'msg': err_msg}
+
+ def _refund_to_merchant(self, order, clkf_phone, tuikuan_liyou):
+ """给商家退余额(事务内调用,不再独立事务)"""
+ try:
+ shangjia_ext = order.shangjia_kuozhan
+ shangjia_id = shangjia_ext.shangjia_id
+ except ObjectDoesNotExist:
+ raise Exception("订单商家信息不存在")
+
+ try:
+ shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
+ shangjia_profile = shangjia_user.shop_profile
+ shangjia_profile.tuikuan += 1
+ shangjia_profile.yue += order.jine or 0
+ shangjia_profile.save()
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ logger.error(f"商家退款:商家 {shangjia_id} 不存在")
+ raise Exception("商家账户不存在")
+
+ def _update_refund_record(self, order, dashou_id, chuli_phone, tuikuan_liyou):
+ """更新退款记录表"""
+ try:
+ #更新支出记录
+ if order.fadan_pingtai == 1:
+ update_daily_payout(order.jine)
+
+ # ========== 新增:商品和店铺每日统计 ==========
+ try:
+ # 商品每日统计(只要有商品ID就执行)
+ if order.shangpin_id:
+ # 从扩展表获取已计算好的收益值(下单时已存储)
+ pingtai_shouyi = Decimal('0.00')
+ dianpu_shouyi = Decimal('0.00')
+ if hasattr(order, 'pingtai_kuozhan'):
+ pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00')
+ dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
+
+ update_shangpin_daily_stat(
+ shangpin_id=order.shangpin_id,
+ caozuo_leixing=3, # 3 = 退款
+ dingdan_jiage=order.jine,
+ pingtai_shouyi=pingtai_shouyi,
+ dianpu_zongshouyi=dianpu_shouyi
+ )
+ logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
+
+ # 店铺每日统计(仅当存在店铺ID时执行)
+ dianpu_id = None
+ if hasattr(order, 'pingtai_kuozhan'):
+ dianpu_id = order.pingtai_kuozhan.dianpu_id
+ if dianpu_id:
+ dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
+ update_dianpu_daily_stat(
+ dianpu_id=dianpu_id,
+ caozuo_leixing=3, # 3 = 退款
+ dingdan_jiage=order.jine,
+ dianpu_shouyi=dianpu_shouyi
+ )
+ logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
+ except Exception as e:
+ logger.error(f"商品/店铺统计更新失败: {e}")
+ logger.error(traceback.format_exc())
+ # 统计失败不影响主流程
+ # =============================================
+
+
+ refund_record, created = Tuikuanjilu.objects.get_or_create(
+ dingdan_id=order.dingdan_id,
+ defaults={
+ 'dashouid': dashou_id or '',
+ 'qingqiuid': '',
+ 'chuliid': chuli_phone,
+ 'liyou': tuikuan_liyou or '客服退款',
+ 'sqzhuangtai': 1,
+ 'jine': order.jine or 0,
+ 'dashou_fencheng': order.dashou_fencheng or 0,
+ 'jieshao': order.jieshao or '',
+ 'beizhu': '客服退款',
+ 'nicheng': order.nicheng or '',
+ }
+ )
+ if not created:
+ refund_record.sqzhuangtai = 1
+ refund_record.chuliid = chuli_phone
+ if tuikuan_liyou:
+ refund_record.liyou = tuikuan_liyou
+ refund_record.save()
+ except Exception as e:
+ logger.error(f"更新退款记录异常: {str(e)}")
+
+ def _generate_nonce_str(self):
+ return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+
+ def _generate_refund_no(self, dingdan_id):
+ timestamp = str(int(time.time()))
+ rand = ''.join(random.choices('0123456789', k=4))
+ return f"{dingdan_id}REF{timestamp}{rand}"
+
+ def _dict_to_xml(self, data):
+ xml = ['']
+ for k, v in data.items():
+ xml.append(f'<{k}>{v}{k}>')
+ xml.append('')
+ return ''.join(xml)
+
+
+
+
+class PartnerGetOrderDataView(APIView):
+ """
+ 对方平台调用,获取我方订单的打手图片和处罚记录
+ 请求:POST /houtai/kpthqtp
+ 参数:{
+ "order_id": "对方平台的订单ID",
+ "club_id": "对方俱乐部ID"
+ }
+ 无需认证
+ 返回:code=0 + images列表 + punishment信息
+ """
+ permission_classes = [AllowAny]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # token = request.data.get('token')
+ # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN):
+ # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200)
+ try:
+ # 1. 获取参数
+ partner_order_id = request.data.get('order_id', '').strip()
+ partner_club_id = request.data.get('club_id', '').strip()
+ if not partner_order_id or not partner_club_id:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 查询我方存储桶域名(自己俱乐部的配置)
+ my_club_config = ClubConfig.objects.filter(is_self=1).first()
+ if not my_club_config:
+ logger.error("未配置我方俱乐部存储桶域名")
+ return Response({'code': 500, 'msg': '系统配置错误'})
+ storage_domain = my_club_config.storage_bucket_domain or ''
+
+ # 3. 查询我方订单(通过对方订单ID和对方俱乐部ID)
+ try:
+ order = Dingdan.objects.get(
+ dingdan_id=partner_order_id,
+ partner_club_id=partner_club_id
+ )
+ except Dingdan.DoesNotExist:
+ logger.warning(f"订单不存在: partner_order_id={partner_order_id}, partner_club_id={partner_club_id}")
+ return Response({'code': 404, 'msg': '订单不存在'})
+
+ dingdan_id = order.dingdan_id
+ dashou_id = order.jiedan_dashou_id
+
+ # 4. 查询打手图片(绝对URL)
+ images = []
+ if dashou_id:
+ dashou_images = Dashoutupian.objects.filter(
+ dingdan_id=dingdan_id,
+ dashou=dashou_id
+ ).values_list('tupian', flat=True)
+ for rel_path in dashou_images:
+ if rel_path:
+ if rel_path.startswith('http'):
+ images.append(rel_path)
+ else:
+ images.append(storage_domain + rel_path)
+
+ # 5. 查询处罚记录(最新一条)
+ punishment = {
+ 'cfliyou': '',
+ 'sqzhuangtai': 0,
+ 'bhliyou': '',
+ 'ssliyou': '',
+ 'jifen': 0
+ }
+ if dashou_id:
+ punish_record = Chufajilu.objects.filter(
+ dingdan_id=dingdan_id,
+ dashouid=dashou_id
+ ).order_by('-create_time').first()
+ if punish_record:
+ punishment = {
+ 'cfliyou': punish_record.cfliyou or '',
+ 'sqzhuangtai': punish_record.sqzhuangtai or 0,
+ 'bhliyou': punish_record.bhliyou or '',
+ 'ssliyou': punish_record.ssliyou or '',
+ 'jifen': punish_record.jifen or 0,
+ }
+
+ # 6. 返回数据
+ return Response({
+ 'code': 0,
+ 'msg': 'success',
+ 'data': {
+ 'images': images,
+ 'punishment': punishment
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"对方获取订单数据接口异常: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+# ========== 新增:对方平台退款通知接口 ==========
+class PartnerRefundNotifyView(APIView):
+ """
+ 接收对方平台推送的订单状态变更通知(完成/退款),同步更新我方数据。
+ 请求:POST /houtai/dfddtk
+ 参数:
+ {
+ "order_id": "对方平台订单ID(实际为我方订单ID)",
+ "club_id": "对方俱乐部ID",
+ "status": 3 或 5,
+ "reason": "理由(可选)"
+ }
+ 返回:code=0 + data包含我方订单当前状态和打手ID
+
+ 核心职责:
+ 1. 更新跨平台订单扩展表
+ 2. 对于对方派单(我方打手接对方的单):
+ a. 状态3(完成)→ 更新订单为已完成,创建预结算(如无)
+ b. 状态5(退款)→ 更新订单为已退款,将预结算待结算改为已拒绝
+ c. 在此过程中,若订单原状态为“进行中(2)”,则将打手状态重置为空闲(1)
+ """
+
+ permission_classes = [AllowAny]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # token = request.data.get('token')
+ # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN):
+ # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200)
+ # 1. 参数校验(事务外执行)
+ partner_order_id = request.data.get('order_id', '').strip()
+ partner_club_id = request.data.get('club_id', '').strip()
+ partner_status = request.data.get('status')
+ reason = request.data.get('reason', '').strip()
+
+ if not partner_order_id or not partner_club_id:
+ return Response({'code': 400, 'msg': '参数不完整'})
+ if partner_status not in (3, 5):
+ return Response({'code': 400, 'msg': '不支持的状态码'})
+
+ # 2. 核心业务在事务内执行
+ try:
+ with transaction.atomic():
+ # 2.1 锁定订单行(必须事务内)
+ try:
+ order = Dingdan.objects.select_for_update().get(
+ dingdan_id=partner_order_id,
+ partner_club_id=partner_club_id
+ )
+ except Dingdan.DoesNotExist:
+ logger.warning(f"通知订单不存在: order_id={partner_order_id}, club_id={partner_club_id}")
+ return Response({'code': 404, 'msg': '订单不存在'})
+
+ my_dingdan_id = order.dingdan_id
+ dashou_id = order.jiedan_dashou_id or ''
+
+ # 记录原订单状态,用于后续打手状态重置
+ original_order_status = order.zhuangtai
+
+ # 2.2 更新跨平台扩展表(记录对方状态)
+ cross_data, created = CrossPlatformOrderData.objects.get_or_create(
+ dingdan_id=my_dingdan_id,
+ defaults={
+ 'partner_order_id': partner_order_id,
+ 'partner_club_id': partner_club_id,
+ 'partner_order_status': partner_status,
+ 'partner_dashou_id': '',
+ 'partner_amount': order.jine or 0,
+ }
+ )
+ if not created:
+ cross_data.partner_order_status = partner_status
+ cross_data.save(update_fields=['partner_order_status', 'update_time'])
+
+ # 2.3 仅处理对方派单(dispatch_type == 2)
+ if order.dispatch_type == 2:
+ # ========== 打手状态恢复逻辑 ==========
+ # 如果原订单状态为“进行中(2)”,且存在接单打手,则将打手状态改为空闲(1)
+ if original_order_status == 2 and dashou_id:
+ try:
+ # 通过打手ID查用户主表,再反向获取打手扩展表
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ dashou_profile.zhuangtai = 1 # 状态置为空闲
+ dashou_profile.save(update_fields=['zhuangtai'])
+ logger.info(f"打手 {dashou_id} 状态已重置为1(空闲)")
+ except UserMain.DoesNotExist:
+ logger.warning(f"打手用户不存在: {dashou_id}")
+ except UserDashou.DoesNotExist:
+ logger.warning(f"打手扩展信息不存在: {dashou_id}")
+ # 🔥 新增:调用打手每日统计(成交)
+ if partner_status ==3:
+
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.jiedan_dashou_id,
+ amount=order.dashou_fencheng,
+ action=2 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ if partner_status ==5:
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.jiedan_dashou_id,
+ amount=order.dashou_fencheng,
+ action=3 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+
+ # ========== 情况A:对方完成(status=3) ==========
+ if partner_status == 3:
+ order.zhuangtai = 3
+
+ order.clkf = partner_club_id # 处理人(对方俱乐部ID)
+ order.save(update_fields=['zhuangtai', 'update_time','clkf'])
+
+ if dashou_id:
+ yjs, yjs_created = PreSettlement.objects.get_or_create(
+ dingdan_id=my_dingdan_id,
+ dashou_id=dashou_id,
+ defaults={
+ 'partner_order_id': partner_order_id,
+ 'partner_club_id': partner_club_id,
+ 'amount': order.dashou_fencheng or 0,
+ 'order_amount': order.jine or 0,
+ 'status': 0, # 待结算
+ 'operator': '',
+ }
+ )
+ if yjs_created:
+ logger.info(f"预结算创建成功: {my_dingdan_id}, 打手 {dashou_id}, 金额 {order.dashou_fencheng}")
+ else:
+ logger.info(f"预结算已存在,跳过: {my_dingdan_id}, 打手 {dashou_id}")
+ else:
+ logger.warning(f"订单 {my_dingdan_id} 无接单打手,跳过预结算创建")
+
+ # ========== 情况B:对方退款(status=5) ==========
+ elif partner_status == 5:
+ order.zhuangtai = 5
+ order.tkly = reason if reason else '' # 退款理由
+ order.clkf = partner_club_id # 处理人(对方俱乐部ID)
+ order.save(update_fields=['zhuangtai', 'update_time','clkf','tkly'])
+
+ if dashou_id:
+ try:
+ yjs = PreSettlement.objects.get(
+ dingdan_id=my_dingdan_id,
+ dashou_id=dashou_id
+ )
+ if yjs.status == 0: # 待结算 → 已拒绝
+ yjs.status = 2
+ yjs.process_time = timezone.now()
+ yjs.save(update_fields=['status', 'process_time', 'update_time'])
+ logger.info(f"预结算状态已改为已拒绝: {my_dingdan_id}")
+ except PreSettlement.DoesNotExist:
+ logger.info(f"订单 {my_dingdan_id} 无预结算记录,无需修改")
+
+ logger.info(f"对方通知处理完成: dingdan_id={my_dingdan_id}, partner_status={partner_status}")
+
+ # 返回结果
+ return Response({
+ 'code': 0,
+ 'msg': 'success',
+ 'data': {
+ 'partner_status': order.zhuangtai,
+ 'partner_dashou_id': dashou_id
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"对方通知接口异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+
+
+
+
+class CrossPlatformForceCompleteView(APIView):
+ """
+ 跨平台订单强制结单接口
+ 请求:POST /houtai/kpqzjd
+ 参数:{"username": "客服账号ID", "dingdan_id": "订单ID"}
+ 认证:JWT
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ username = request.data.get('username', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ if not username or not dingdan_id:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 身份验证
+ current_user = request.user
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '003aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查处理订单'})
+
+ # 4. 查询订单并加锁
+ try:
+ with transaction.atomic():
+ order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
+
+ # 5. 校验订单状态必须为8(结算中)
+ if order.zhuangtai not in (2,8):
+ return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'})
+
+ # 获取关键字段
+ dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单
+ is_cross = order.is_cross # 0=本地,1=跨平台
+ partner_club_id = order.partner_club_id
+ partner_order_id = order.partner_order_id
+ dashou_id = order.jiedan_dashou_id
+ dashou_fencheng = order.dashou_fencheng or Decimal('0')
+ jine = order.jine or Decimal('0')
+ fadan_pingtai = order.fadan_pingtai # 1=平台,2=商家
+
+ # ========== 修复点1:完善跨平台判断 ==========
+ # 跨平台且被对方打手接单的条件:is_cross==1 且 (partner_club_id 或 partner_order_id 有值)
+ is_cross_claimed_by_partner = (is_cross == 1 and (partner_club_id or partner_order_id))
+ # 如果是跨平台订单但两个字段都为空,则按本地订单处理(即自家打手接单)
+
+ # ========== 修复点2:根据方向处理 ==========
+ # ---------- 对方派单(我方打手接对方单)----------
+ if dispatch_type == 2:
+ # 创建预结算记录(如果已存在则更新)
+ pre_settlement, created = PreSettlement.objects.get_or_create(
+ dingdan_id=dingdan_id,
+ defaults={
+ 'partner_order_id': partner_order_id,
+ 'partner_club_id': partner_club_id,
+ 'dashou_id': dashou_id,
+ 'amount': dashou_fencheng,
+ 'order_amount': jine,
+ 'status': 0, # 待结算
+ 'operator': current_user.phone,
+ }
+ )
+ if not created:
+ pre_settlement.partner_order_id = partner_order_id
+ pre_settlement.partner_club_id = partner_club_id
+ pre_settlement.dashou_id = dashou_id
+ pre_settlement.amount = dashou_fencheng
+ pre_settlement.order_amount = jine
+ pre_settlement.status = 0
+ pre_settlement.process_time = None
+ pre_settlement.operator = current_user.phone
+ pre_settlement.save()
+
+ # 更新订单状态
+ order.zhuangtai = 3
+ order.clkf = current_user.phone
+ order.save()
+
+ # 更新每日统计表(对方派单方向,使用订单金额)
+ if partner_club_id:
+ # 修复点3:使用公共统计函数
+ update_daily_dispatch_stat(
+ direction=2,
+ partner_club_id=partner_club_id,
+ success_amount=jine,
+ success_count=1
+ )
+ else:
+ # 如果没有俱乐部ID,使用默认000001
+ update_daily_dispatch_stat(
+ direction=2,
+ partner_club_id='000001',
+ success_amount=jine,
+ success_count=1
+ )
+
+ # 通知对方平台订单已完成(修复点4:移到事务外)
+ if partner_club_id and partner_order_id:
+ transaction.on_commit(
+ lambda: self._notify_partner_order_status(
+ partner_club_id=partner_club_id,
+ partner_order_id=partner_order_id,
+ new_status=3,
+ current_user=current_user
+ )
+ )
+
+ # ---------- 我方派单 ----------
+ elif dispatch_type == 1:
+ # 判断是否为跨平台订单且被对方打手接单(即 partner_club_id 或 partner_order_id 有值)
+ if is_cross_claimed_by_partner:
+ # 跨平台订单:我方派单给外平台打手
+ # 修复点5:处理只有 partner_order_id 没有 partner_club_id 的情况
+ real_partner_club_id = partner_club_id or '000001'
+
+ # 更新订单状态
+ order.zhuangtai = 3
+ order.clkf = current_user.phone
+ order.save()
+
+ # 更新每日统计表(我方派单方向,使用打手分成金额)
+ update_daily_dispatch_stat(
+ direction=1,
+ partner_club_id=real_partner_club_id,
+ success_amount=dashou_fencheng,
+ success_count=1
+ )
+
+
+ # ========== 新增:商品和店铺每日统计 ==========
+ if order.fadan_pingtai == 1:
+ try:
+ # 商品每日统计(只要有商品ID就执行)
+ if order.shangpin_id:
+ # 从扩展表获取已计算好的收益值(下单时已存储)
+ pingtai_shouyi = Decimal('0.00')
+ dianpu_shouyi = Decimal('0.00')
+ if hasattr(order, 'pingtai_kuozhan'):
+ pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00')
+ dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
+
+ update_shangpin_daily_stat(
+ shangpin_id=order.shangpin_id,
+ caozuo_leixing=3, # 3 = 退款
+ dingdan_jiage=order.jine,
+ pingtai_shouyi=pingtai_shouyi,
+ dianpu_zongshouyi=dianpu_shouyi
+ )
+ logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
+
+ # 店铺每日统计(仅当存在店铺ID时执行)
+ dianpu_id = None
+ if hasattr(order, 'pingtai_kuozhan'):
+ dianpu_id = order.pingtai_kuozhan.dianpu_id
+ if dianpu_id:
+ dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
+ update_dianpu_daily_stat(
+ dianpu_id=dianpu_id,
+ caozuo_leixing=3, # 3 = 退款
+ dingdan_jiage=order.jine,
+ dianpu_shouyi=dianpu_shouyi
+ )
+ logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
+ except Exception as e:
+ logger.error(f"商品/店铺统计更新失败: {e}")
+ logger.error(traceback.format_exc())
+ # 统计失败不影响主流程
+ # =============================================
+
+ # 通知对方平台订单已完成(异步)
+ if partner_order_id:
+ transaction.on_commit(
+ lambda: self._notify_partner_order_status(
+ partner_club_id=real_partner_club_id,
+ partner_order_id=partner_order_id,
+ new_status=3,
+ current_user=current_user
+ )
+ )
+ else:
+ # 本地订单(非跨平台 或 跨平台但实际是自家打手接单),按原有逻辑给自家打手结算
+ if not dashou_id:
+ return Response({'code': 400, 'msg': '订单无接单打手'})
+ if dashou_fencheng <= 0:
+ return Response({'code': 400, 'msg': '打手分成金额无效'})
+
+ # ========== 新增:商品和店铺每日统计 ==========
+ if order.fadan_pingtai == 1:
+ try:
+ # 商品每日统计(只要有商品ID就执行)
+ if order.shangpin_id:
+ # 从扩展表获取已计算好的收益值(下单时已存储)
+ pingtai_shouyi = Decimal('0.00')
+ dianpu_shouyi = Decimal('0.00')
+ if hasattr(order, 'pingtai_kuozhan'):
+ pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00')
+ dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
+
+ update_shangpin_daily_stat(
+ shangpin_id=order.shangpin_id,
+ caozuo_leixing=3, # 3 = 退款
+ dingdan_jiage=order.jine,
+ pingtai_shouyi=pingtai_shouyi,
+ dianpu_zongshouyi=dianpu_shouyi
+ )
+ logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
+
+ # 店铺每日统计(仅当存在店铺ID时执行)
+ dianpu_id = None
+ if hasattr(order, 'pingtai_kuozhan'):
+ dianpu_id = order.pingtai_kuozhan.dianpu_id
+ if dianpu_id:
+ dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
+ update_dianpu_daily_stat(
+ dianpu_id=dianpu_id,
+ caozuo_leixing=3, # 3 = 退款
+ dingdan_jiage=order.jine,
+ dianpu_shouyi=dianpu_shouyi
+ )
+ logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
+ except Exception as e:
+ logger.error(f"商品/店铺统计更新失败: {e}")
+ logger.error(traceback.format_exc())
+ # 统计失败不影响主流程
+ # =============================================
+
+ # 更新打手余额和统计
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ dashou_profile.chengjiaozongliang += 1
+ dashou_profile.yue += dashou_fencheng
+ dashou_profile.zonge += dashou_fencheng
+ dashou_profile.jinrishouyi += dashou_fencheng
+ dashou_profile.jinyueshouyi += dashou_fencheng
+ dashou_profile.save()
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"本地订单结算:打手 {dashou_id} 不存在,跳过")
+
+ # 如果是商家订单,更新商家成交单量
+ if fadan_pingtai == 2:
+ try:
+ shangjia_ext = order.shangjia_kuozhan
+ if shangjia_ext and shangjia_ext.shangjia_id:
+ shangjia_user = UserMain.objects.get(yonghuid=shangjia_ext.shangjia_id)
+ shangjia_profile = shangjia_user.shop_profile
+ shangjia_profile.chengjiao += 1
+ shangjia_profile.save()
+ except Exception as e:
+ logger.warning(f"商家订单更新失败: {str(e)}")
+
+ # 更新订单状态
+ order.zhuangtai = 3
+ order.clkf = current_user.phone
+ order.save()
+
+ # 本地订单不涉及跨平台统计,无需更新 DailyDispatchStat
+
+ else:
+ return Response({'code': 400, 'msg': '未知的派单方向'})
+
+
+ if dispatch_type == 1 and is_cross_claimed_by_partner:
+ pass
+ # 判断是否为跨平台订单且被对方打手接单(即 partner_club_id 或 partner_order_id 有值)
+ elif dispatch_type == 2:
+ pass
+ else:
+
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.jiedan_dashou_id,
+ amount=dashou_fencheng,
+ action=2 # 2 表示成交(结算)
+ )
+ logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+ if order.fadan_pingtai == 2:
+ shangjia_ext = order.shangjia_kuozhan
+ # ✅ 新方法(行为驱动,更安全)
+ update_shangjia_daily(
+ yonghuid=shangjia_ext.shangjia_id,
+ amount=Decimal(str(order.jine)),
+ action=2 # 2 = 结算
+ )
+ else:
+ pass
+
+ # 7. 更新客服统计
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ # 8. 记录操作日志
+ logger.info(f"强制结单成功,订单 {dingdan_id},方向 {dispatch_type},客服 {username}")
+
+ return Response({'code': 0, 'msg': '强制结单成功'})
+
+ except Dingdan.DoesNotExist:
+ logger.warning(f"订单不存在: {dingdan_id}")
+ return Response({'code': 404, 'msg': '订单不存在'})
+ except Exception as e:
+ logger.error(f"强制结单接口异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+ # ========== 辅助方法 ==========
+ def _notify_partner_order_status(self, partner_club_id, partner_order_id, new_status, current_user):
+ """
+ 通知对方平台订单状态变更(已在事务外调用)
+ """
+ try:
+ # 获取我方俱乐部ID
+ my_club_config = ClubConfig.objects.filter(is_self=1).first()
+ if not my_club_config:
+ logger.error("未配置我方俱乐部信息,无法通知对方")
+ return
+ self_club_id = my_club_config.club_id
+
+ # 查询对方服务器域名
+ club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
+ partner_domain = club_rel.partner_domain
+ if not partner_domain:
+ logger.error(f"对方俱乐部 {partner_club_id} 未配置域名")
+ return
+
+ url = partner_domain.rstrip('/') + '/houtai/dfddtk'
+ payload = {
+ 'order_id': partner_order_id,
+ 'club_id': self_club_id,
+ 'status': new_status
+ }
+ resp = requests.post(url, json=payload, timeout=3)
+ if resp.status_code == 200:
+ resp_json = resp.json()
+ if resp_json.get('code') == 0:
+ # 更新跨平台订单扩展表中的对方状态
+ cross_data, _ = CrossPlatformOrderData.objects.get_or_create(
+ dingdan_id=partner_order_id,
+ defaults={
+ 'partner_order_id': partner_order_id,
+ 'partner_club_id': partner_club_id,
+ 'partner_order_status': new_status,
+ 'partner_dashou_id': '',
+ 'partner_amount': Decimal('0')
+ }
+ )
+ if not _:
+ cross_data.partner_order_status = new_status
+ cross_data.save(update_fields=['partner_order_status'])
+ logger.info(f"通知对方订单状态成功: {partner_order_id} -> {new_status}")
+ else:
+ logger.warning(f"对方接口返回错误: {resp_json.get('msg')}")
+ else:
+ logger.warning(f"对方接口HTTP错误: {resp.status_code}")
+ except requests.exceptions.Timeout:
+ logger.error(f"通知对方平台超时: {partner_club_id}")
+ except Exception as e:
+ logger.error(f"通知对方平台异常: {str(e)}")
+
+
+
+
+class CrossPlatformTransferHallView(APIView):
+ """
+ 跨平台订单转移大厅接口
+ 请求:POST /houtai/kpzydt
+ 参数:{"username": "客服账号ID", "dingdan_id": "订单ID"}
+ 认证:JWT
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ username = request.data.get('username', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ if not username or not dingdan_id:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 客服身份验证
+ current_user = request.user
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '003aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权处理俱乐部订单'})
+
+ # 4. 查询订单并加锁
+ try:
+ with transaction.atomic():
+ order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
+
+ # 5. 状态校验
+ if order.zhuangtai not in [2, 8, 4]:
+ return Response({'code': 400, 'msg': f'订单状态不允许转移大厅,当前状态: {order.zhuangtai}'})
+
+ # 获取关键字段
+ is_cross = order.is_cross
+ dispatch_type = order.dispatch_type
+ partner_club_id = order.partner_club_id
+ partner_order_id = order.partner_order_id
+ current_dashou_id = order.jiedan_dashou_id
+ zhiding_id = order.zhiding_id
+
+ # ========== 判断是否需要通知对方平台 ==========
+ # 条件:跨平台 且 我方派单(dispatch_type=1) 且 (partner_club_id 或 partner_order_id 有值)
+ need_notify = (is_cross == 1 and (partner_club_id or partner_order_id))
+
+ # ========== 判断是否需要修改本地打手状态 ==========
+ # 当接单打手是我方打手时,才需要修改状态(改为空闲1)
+ # 我方打手的情况:
+ # 1. 非跨平台订单 (is_cross != 1)
+ # 2. 跨平台且对方派单 (dispatch_type == 2) —— 此时打手是我方
+ # 3. 跨平台且我方派单但没有对方信息 (即 need_notify == False) —— 此时打手也是我方
+ need_update_dashou_status = (current_dashou_id and
+ (is_cross != 1 or dispatch_type == 2 or not need_notify))
+
+ # ========== 记录打手历史(无论打手是谁,只要有ID就记录) ==========
+ if current_dashou_id:
+ last_history = OrderDashouHistory.objects.filter(
+ dingdan_id=dingdan_id
+ ).order_by('-times').first()
+ next_times = (last_history.times + 1) if last_history else 1
+ OrderDashouHistory.objects.create(
+ dingdan_id=dingdan_id,
+ dashou_id=current_dashou_id,
+ times=next_times,
+ operator=username
+ )
+
+ # ========== 修改本地打手状态(仅当是我方打手时) ==========
+ if need_update_dashou_status:
+ try:
+ dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id)
+ dashou_profile.zhuangtai = 1
+ dashou_profile.save()
+ logger.info(f"打手 {current_dashou_id} 状态改为空闲")
+ except ObjectDoesNotExist:
+ logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新")
+
+ # ========== 更新订单状态(清空打手,改为1或7) ==========
+ if zhiding_id:
+ order.zhuangtai = 7 # 指定中
+ else:
+ order.zhuangtai = 1 # 下单中
+ order.jiedan_dashou_id = None
+ order.clkf = username
+ order.save()
+
+ # ========== 如果需要通知对方平台,异步发送通知 ==========
+ if need_notify:
+ # 确定对方俱乐部ID(如果没有则用默认000001)
+ real_partner_club_id = partner_club_id if partner_club_id else '000001'
+ real_partner_order_id = partner_order_id if partner_order_id else dingdan_id
+
+ # 获取我方俱乐部ID
+ my_club_config = ClubConfig.objects.filter(is_self=1).first()
+ if my_club_config:
+ self_club_id = my_club_config.club_id
+ # 查询对方域名
+ try:
+ club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=real_partner_club_id)
+ partner_domain = club_rel.partner_domain
+ if partner_domain:
+ url = partner_domain.rstrip('/') + '/houtai/kpttzzydt'
+ payload = {
+ 'order_id': real_partner_order_id,
+ 'club_id': self_club_id
+ }
+
+ def notify():
+ try:
+ resp = requests.post(url, json=payload, timeout=3)
+ if resp.status_code == 200:
+ resp_json = resp.json()
+ if resp_json.get('code') == 0:
+ # 更新跨平台订单扩展表中的对方状态
+ cross_data, created = CrossPlatformOrderData.objects.get_or_create(
+ dingdan_id=dingdan_id,
+ defaults={
+ 'partner_order_id': real_partner_order_id,
+ 'partner_club_id': real_partner_club_id,
+ 'partner_order_status': resp_json.get('data', {}).get('partner_status', 0),
+ 'partner_dashou_id': '',
+ 'partner_amount': order.jine or 0,
+ }
+ )
+ if not created:
+ cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0)
+ cross_data.save(update_fields=['partner_order_status'])
+ logger.info(f"转移大厅通知对方成功: {dingdan_id}")
+
+ # ========== 新增代码开始 ==========
+ # 对方成功转移大厅,清空我方订单中的对方相关信息
+ # ========== 新增代码开始 ==========
+ # 只有我方派单时才清空对方平台信息,对方派单则保留
+ if dispatch_type == 1:
+ Dingdan.objects.filter(dingdan_id=dingdan_id).update(
+ partner_club_id=None,
+ partner_order_id=None,
+ jiedan_dashou_id=None,
+ zhuangtai=1
+ )
+ logger.info(f"我方派单,已清空订单 {dingdan_id} 的对方信息")
+ else:
+ # 对方派单,只清空打手并恢复状态,保留对方信息
+ Dingdan.objects.filter(dingdan_id=dingdan_id).update(
+ jiedan_dashou_id=None,
+ zhuangtai=1
+ )
+ logger.info(f"对方派单,只清空打手,保留对方信息,订单 {dingdan_id}")
+ # ========== 新增代码结束 ==========
+ # ========== 新增代码结束 ==========
+
+ else:
+ logger.warning(f"对方平台返回错误: {resp_json.get('msg')}")
+ else:
+ logger.warning(f"对方接口HTTP错误: {resp.status_code}")
+ except Exception as e:
+ logger.error(f"转移大厅通知对方异常: {str(e)}")
+
+ threading.Thread(target=notify).start()
+ except Club.DoesNotExist:
+ logger.error(f"未找到对方俱乐部配置: partner_club_id={real_partner_club_id}")
+ except Exception as e:
+ logger.error(f"查询对方平台配置失败: {str(e)}")
+
+ # ========== 更新客服统计 ==========
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ return Response({'code': 0, 'msg': '转移大厅成功'})
+
+ except Dingdan.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'})
+ except Exception as e:
+ logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+
+class PartnerTransferHallNotifyView(APIView):
+ """
+ 对方平台转移大厅通知接口
+ 对方调用此接口通知我方订单已转移大厅,我方同步更新订单状态
+ 请求:POST /houtai/kpttzzydt
+ 参数:{
+ "order_id": "对方平台的订单ID",
+ "club_id": "对方俱乐部ID"
+ }
+ 无需认证
+ 返回:{
+ "code": 0,
+ "msg": "success",
+ "data": {
+ "partner_status": 1 # 我方订单当前状态
+ }
+ }
+ """
+ parser_classes = [JSONParser]
+ permission_classes = [AllowAny]
+
+ def post(self, request):
+ # token = request.data.get('token')
+ # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN):
+ # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200)
+ try:
+ # 1. 获取参数
+ partner_order_id = request.data.get('order_id', '').strip()
+ partner_club_id = request.data.get('club_id', '').strip()
+ if not partner_order_id or not partner_club_id:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 查询我方订单
+ try:
+ order = Dingdan.objects.get(
+ dingdan_id=partner_order_id,
+ partner_club_id=partner_club_id
+ )
+ except Dingdan.DoesNotExist:
+ logger.warning(f"对方转移大厅通知:订单不存在 partner_order_id={partner_order_id}, partner_club_id={partner_club_id}")
+ return Response({'code': 404, 'msg': '订单不存在'})
+
+ # 3. 加锁处理
+ with transaction.atomic():
+ order = Dingdan.objects.select_for_update().get(id=order.id)
+
+ current_status = order.zhuangtai
+ current_dashou_id = order.jiedan_dashou_id
+
+ # 4. 根据订单状态处理
+ if current_status == 3:
+ # 已完成状态,需要检查预结算记录
+ try:
+ pre_settlement = PreSettlement.objects.select_for_update().get(
+ dingdan_id=order.dingdan_id,
+ dashou_id=current_dashou_id
+ )
+ # 如果预结算状态是0(待结算),改为2(已拒绝)
+ if pre_settlement.status == 0:
+ pre_settlement.status = 2
+ pre_settlement.process_time = timezone.now()
+ pre_settlement.operator = partner_club_id # 记录操作方
+ pre_settlement.save()
+ logger.info(f"预结算记录已拒绝: {order.dingdan_id}")
+ else:
+ logger.info(f"预结算记录已是其他状态 {pre_settlement.status},不重复修改")
+ except PreSettlement.DoesNotExist:
+ logger.warning(f"订单 {order.dingdan_id} 已完成但无预结算记录,无法转移大厅")
+ return Response({'code': 400, 'msg': '订单已完成且无预结算记录,无法转移大厅'})
+
+ # 5. 记录打手历史(如果有接单打手)
+ if current_dashou_id:
+ last_history = OrderDashouHistory.objects.filter(
+ dingdan_id=order.dingdan_id
+ ).order_by('-times').first()
+ next_times = (last_history.times + 1) if last_history else 1
+ OrderDashouHistory.objects.create(
+ dingdan_id=order.dingdan_id,
+ dashou_id=current_dashou_id,
+ times=next_times,
+ operator=partner_club_id # 记录操作方
+ )
+ # 将打手状态改为空闲
+ try:
+ dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id)
+ dashou_profile.zhuangtai = 1
+ dashou_profile.save()
+
+ # 打手退款每日统计
+ if order.dispatch_type == 2:
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=current_dashou_id,
+ amount=dashou_profile.dashou_fencheng,
+ action=3 # 退款
+ )
+ except Exception as e:
+ logger.error(f"记录打手统计错误: {str(e)}")
+ except ObjectDoesNotExist:
+ logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新")
+
+ # 6. 更新订单状态为1(下单中)或7(指定中)
+ # 7. 根据派单方向清理对方信息
+ if order.dispatch_type == 1:
+ # 我方派单:清空对方俱乐部ID和订单ID
+ order.partner_club_id = None
+ order.partner_order_id = None
+ # 对方派单时保留对方信息,不清空
+
+ # 8. 更新订单状态
+ if order.zhiding_id:
+ order.zhuangtai = 7 # 指定中
+ else:
+ order.zhuangtai = 1 # 下单中
+
+
+ order.jiedan_dashou_id = None
+ order.clkf = partner_club_id # 记录操作方
+ order.save()
+
+ logger.info(f"对方转移大厅通知成功: 订单 {order.dingdan_id},新状态 {order.zhuangtai}")
+
+ # 7. 返回我方订单状态
+ return Response({
+ 'code': 0,
+ 'msg': 'success',
+ 'data': {
+ 'partner_status': order.zhuangtai
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"对方转移大厅通知接口异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+
+
+
+
+
+logger = logging.getLogger(__name__)
+
+
+# ========== 1. 获取俱乐部设置数据接口 ==========
+class GetSettingsView(APIView):
+ """
+ 获取跨平台设置页面数据
+ 请求:POST /houtai/hqkptsz
+ 参数:{"username": "客服账号ID"}
+ 权限:权限码2且开启
+ 返回:包含自己俱乐部信息(含客服字段)和合作俱乐部列表
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 客服身份验证
+ current_user = request.user
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '004aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
+
+ # 4. 查询数据
+ try:
+ # 4.1 自己俱乐部配置(is_self=1)
+ self_config = ClubConfig.objects.filter(is_self=1).first()
+ if not self_config:
+ logger.error("未找到自己俱乐部配置")
+ return Response({'code': 500, 'msg': '系统配置错误'})
+
+ self_club = {
+ 'club_id': self_config.club_id,
+ 'club_nickname': self_config.club_nickname,
+ 'club_avatar': self_config.club_avatar,
+ 'backend_domain': self_config.backend_domain,
+ 'chat_api_key': self_config.chat_api_key,
+ 'chat_api_id': self_config.chat_api_id,
+ 'customer_service_api_url': self_config.customer_service_api_url,
+ 'customer_service_corp_id': self_config.customer_service_corp_id,
+ 'storage_bucket_domain': self_config.storage_bucket_domain,
+ }
+
+ # 4.2 合作俱乐部列表(Club 表中本俱乐部ID = self_config.club_id)
+ partner_records = Club.objects.filter(club_id=self_config.club_id)
+ partner_clubs = []
+ for record in partner_records:
+ partner_config = ClubConfig.objects.filter(
+ club_id=record.partner_club_id,
+ is_self=0
+ ).first()
+ partner_clubs.append({
+ 'club_id': record.partner_club_id,
+ 'club_nickname': partner_config.club_nickname if partner_config else record.partner_club_id,
+ 'club_avatar': partner_config.club_avatar if partner_config else '',
+ 'is_interop_enabled': record.is_interop_enabled,
+ 'is_receive_enabled': record.is_receive_enabled,
+ })
+
+ return Response({'code': 0, 'data': {'self_club': self_club, 'partner_clubs': partner_clubs}})
+
+ except Exception as e:
+ logger.error(f"获取设置失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+# ========== 2. 获取订单类型映射接口 ==========
+class GetMappingsView(APIView):
+ """
+ 获取指定合作俱乐部的订单类型映射
+ 请求:POST /houtai/hqqtjlbsj
+ 参数:{"username": "客服账号ID", "partner_club_id": "对方俱乐部ID"}
+ 权限:权限码2
+ 返回:对方订单类型列表、我方订单类型列表、当前映射关系
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ username = request.data.get('username', '').strip()
+ partner_club_id = request.data.get('partner_club_id', '').strip()
+ if not username or not partner_club_id:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 客服身份验证
+ current_user = request.user
+
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '004aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
+
+ try:
+ # 4. 获取我方所有商品类型(订单类型)
+ our_types = ShangpinLeixing.objects.all().values('id', 'jieshao', 'tupian_url')
+ our_type_list = [
+ {'type_id': t['id'], 'type_name': t['jieshao'], 'icon': t['tupian_url']}
+ for t in our_types
+ ]
+
+ # 5. 获取对方订单类型(从映射表去重)
+ mappings_query = OrderTypeMapping.objects.filter(partner_club_id=partner_club_id)
+ partner_types_dict = {}
+ for m in mappings_query:
+ partner_types_dict[m.partner_type_id] = {
+ 'type_id': m.partner_type_id,
+ 'type_name': m.partner_type_name,
+ 'icon': ''
+ }
+ partner_type_list = list(partner_types_dict.values())
+
+ # 6. 获取当前映射关系
+ mapping_list = [
+ {'partner_type_id': m.partner_type_id, 'our_type_id': m.our_type_id}
+ for m in mappings_query
+ ]
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'partner_types': partner_type_list,
+ 'our_types': our_type_list,
+ 'mappings': mapping_list
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"获取映射失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+# ========== 3. 保存设置接口(自己俱乐部 + 合作俱乐部开关)==========
+class UpdateSettingsView(APIView):
+ """
+ 保存俱乐部设置(自己俱乐部信息 + 合作俱乐部开关)
+ 请求:POST /houtai/xgjlbsj
+ 参数:{
+ "username": "客服账号ID",
+ "self_club": {
+ "club_nickname": "...",
+ "backend_domain": "...",
+ "chat_api_key": "...",
+ "chat_api_id": "...",
+ "customer_service_api_url": "...",
+ "customer_service_corp_id": "...",
+ "storage_bucket_domain": "..."
+ },
+ "partner_updates": [
+ {"club_id": "654321", "is_interop_enabled": true, "is_receive_enabled": false}
+ ]
+ }
+ 权限:权限码2
+ 返回:{"code": 0, "msg": "success"}
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 客服身份验证
+ current_user = request.user
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '004aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权修改俱乐部配置'})
+
+ # 4. 获取自己俱乐部当前配置
+ try:
+ self_config = ClubConfig.objects.get(is_self=1)
+ except ObjectDoesNotExist:
+ logger.error("未找到自己俱乐部配置")
+ return Response({'code': 500, 'msg': '系统配置错误'})
+
+ # 5. 更新自己俱乐部配置(包含客服字段)
+ self_updates = request.data.get('self_club', {})
+ if self_updates:
+ allowed_fields = [
+ 'club_nickname', 'backend_domain', 'chat_api_key', 'chat_api_id',
+ 'customer_service_api_url', 'customer_service_corp_id',
+ 'storage_bucket_domain'
+ ]
+ fields_to_update = []
+ for field in allowed_fields:
+ if field in self_updates:
+ setattr(self_config, field, self_updates[field])
+ fields_to_update.append(field)
+ if fields_to_update:
+ self_config.save(update_fields=fields_to_update)
+
+ # 6. 更新合作俱乐部开关
+ partner_updates = request.data.get('partner_updates', [])
+ for update in partner_updates:
+ club_id = update.get('club_id')
+ if not club_id:
+ continue
+ try:
+ partner_record = Club.objects.get(club_id=self_config.club_id, partner_club_id=club_id)
+ if 'is_interop_enabled' in update:
+ partner_record.is_interop_enabled = update['is_interop_enabled']
+ if 'is_receive_enabled' in update:
+ partner_record.is_receive_enabled = update['is_receive_enabled']
+ partner_record.save(update_fields=['is_interop_enabled', 'is_receive_enabled'])
+ except ObjectDoesNotExist:
+ logger.warning(f"合作俱乐部记录不存在: {club_id}")
+
+ # 7. 异步通知所有合作俱乐部(如果自己数据有修改)
+ if self_updates:
+ self._notify_all_partners(self_config, self_updates)
+
+ return Response({'code': 0, 'msg': 'success'})
+
+ def _notify_all_partners(self, self_config, updates):
+ """异步通知所有合作俱乐部更新我方配置"""
+ partner_records = Club.objects.filter(club_id=self_config.club_id)
+ if not partner_records:
+ return
+
+ data_to_send = {
+ 'club_id': self_config.club_id,
+ 'updates': updates
+ }
+
+ def notify(partner_domain):
+ try:
+ url = partner_domain.rstrip('/') + '/houtai/xgzjsjcs'
+ resp = requests.post(url, json=data_to_send, timeout=3)
+ if resp.status_code != 200:
+ logger.warning(f"通知 {partner_domain} 失败,状态码 {resp.status_code}")
+ except Exception as e:
+ logger.error(f"通知 {partner_domain} 异常: {str(e)}")
+
+ for record in partner_records:
+ domain = record.partner_domain
+ if domain:
+ threading.Thread(target=notify, args=(domain,)).start()
+
+
+
+
+
+class UpdateMappingsView(APIView):
+ """
+ 保存指定合作俱乐部的订单类型映射
+ 请求:POST /houtai/xgdfddys
+ 参数:{
+ "username": "客服手机号",
+ "partner_club_id": "对方俱乐部ID",
+ "mappings": [
+ {"partner_type_id": 1, "our_type_id": 10},
+ {"partner_type_id": 2, "our_type_id": 11}
+ ]
+ }
+ 权限:权限码2
+ 返回:{"code": 0, "msg": "success"}
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ username = request.data.get('username', '').strip()
+ partner_club_id = request.data.get('partner_club_id', '').strip()
+ mappings = request.data.get('mappings', [])
+
+ if not username or not partner_club_id:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 客服身份验证
+ current_user = request.user
+
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '004aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权修改俱乐部配置'})
+
+ try:
+ with transaction.atomic():
+ # 4. 获取当前该俱乐部所有现有映射(按 partner_type_id 索引)
+ existing_mappings = {
+ m.partner_type_id: m
+ for m in OrderTypeMapping.objects.filter(partner_club_id=partner_club_id)
+ }
+
+ # 5. 处理前端传来的映射(更新或新增)
+ processed_partner_ids = set()
+ for mapping in mappings:
+ partner_type_id = mapping.get('partner_type_id')
+ our_type_id = mapping.get('our_type_id')
+ if partner_type_id is None or our_type_id is None:
+ continue
+ processed_partner_ids.add(partner_type_id)
+
+ # 获取我方类型名称
+ our_type_name = ''
+ try:
+ our_type = ShangpinLeixing.objects.get(id=our_type_id)
+ our_type_name = our_type.jieshao
+ except ObjectDoesNotExist:
+ logger.warning(f"我方订单类型不存在: {our_type_id}")
+
+ # 查找是否存在该对方类型的映射
+ if partner_type_id in existing_mappings:
+ # 更新现有记录
+ mapping_obj = existing_mappings[partner_type_id]
+ mapping_obj.our_type_id = our_type_id
+ mapping_obj.our_type_name = our_type_name
+ mapping_obj.save(update_fields=['our_type_id', 'our_type_name'])
+ # 从字典中移除,以便后续知道哪些被删除了
+ del existing_mappings[partner_type_id]
+ else:
+ # 新增记录
+ # 对方类型名称:优先从现有映射中查找(可能被其他俱乐部共享?不,每个俱乐部独立)
+ # 由于该对方类型没有映射过,我们暂时无法获取名称,可留空或使用默认值。
+ # 但为保证数据完整性,我们尝试从已有记录中查找(如果该俱乐部之前删除过又重建,则可能丢失名称)
+ # 更稳妥的方式是前端传递 partner_type_name,但当前未要求,因此此处留空并记录警告。
+ partner_type_name = ''
+ logger.warning(f"新增映射时对方类型名称缺失: partner_club_id={partner_club_id}, partner_type_id={partner_type_id}")
+
+ OrderTypeMapping.objects.create(
+ partner_club_id=partner_club_id,
+ partner_type_id=partner_type_id,
+ partner_type_name=partner_type_name,
+ our_type_id=our_type_id,
+ our_type_name=our_type_name
+ )
+
+ # 6. 删除前端未传入的映射(即用户取消的映射)
+ for partner_type_id, mapping_obj in existing_mappings.items():
+ mapping_obj.delete()
+ logger.info(f"删除映射: partner_club_id={partner_club_id}, partner_type_id={partner_type_id}")
+
+ return Response({'code': 0, 'msg': 'success'})
+
+ except Exception as e:
+ logger.error(f"保存映射失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+# ========== 5. 接收对方平台修改数据接口 ==========
+class PartnerUpdateSelfConfigView(APIView):
+ """
+ 接收对方平台修改自己俱乐部配置的通知
+ 请求:POST /houtai/xgzjsjcs
+ 参数:{
+ "club_id": "对方俱乐部ID",
+ "updates": {
+ "club_nickname": "新昵称",
+ "backend_domain": "新域名",
+ "chat_api_key": "新key",
+ "chat_api_id": "新id",
+ "customer_service_api_url": "新URL",
+ "customer_service_corp_id": "新企业ID",
+ "storage_bucket_domain": "新存储桶"
+ }
+ }
+ 无需认证,对方平台可信调用
+ 返回:{"code": 0, "msg": "success"}
+ """
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # token = request.data.get('token')
+ # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN):
+ # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200)
+ # 1. 获取参数
+ club_id = request.data.get('club_id', '').strip()
+ updates = request.data.get('updates', {})
+
+ if not club_id or not updates:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 查询对方俱乐部的配置记录(is_self=0)
+ try:
+ config = ClubConfig.objects.get(club_id=club_id, is_self=0)
+ except ClubConfig.DoesNotExist:
+ logger.warning(f"对方俱乐部配置不存在: {club_id}")
+ return Response({'code': 404, 'msg': '俱乐部不存在'})
+
+ # 3. 允许修改的字段白名单(包含客服字段)
+ allowed_fields = [
+ 'club_nickname',
+ 'backend_domain',
+ 'chat_api_key',
+ 'chat_api_id',
+ 'customer_service_api_url',
+ 'customer_service_corp_id',
+ 'storage_bucket_domain'
+ ]
+
+ # 4. 更新字段
+ with transaction.atomic():
+ fields_updated = []
+ for field in allowed_fields:
+ if field in updates:
+ new_value = updates[field]
+ setattr(config, field, new_value)
+ fields_updated.append(field)
+ if fields_updated:
+ config.save(update_fields=fields_updated)
+ logger.info(f"对方俱乐部 {club_id} 配置已更新: {fields_updated}")
+ else:
+ logger.info(f"对方俱乐部 {club_id} 无有效字段更新")
+
+ return Response({'code': 0, 'msg': 'success'})
+
+
+
+
+# apps/houtai/views.py
+
+
+
+
+
+# ========== 1. 获取合作俱乐部列表接口(完整) ==========
+class GetPartnerClubsView(APIView):
+ """
+ 获取我方合作俱乐部列表(用于统计页面)
+ 请求:POST /houtai/kpthqpt
+ 参数:{"username": "客服手机号"}
+ 权限:权限码2且开启
+ 返回:{"code":0, "data":[{"club_id":"xxx","club_nickname":"xxx","club_avatar":"xxx"}]}
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 客服身份验证
+ current_user = request.user
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '004bb' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
+
+ # 4. 获取我方俱乐部ID
+ try:
+ self_config = ClubConfig.objects.get(is_self=1)
+ except ObjectDoesNotExist:
+ logger.error("未找到自己俱乐部配置")
+ return Response({'code': 500, 'msg': '系统配置错误'})
+
+ # 5. 查询所有合作俱乐部
+ try:
+ records = Club.objects.filter(club_id=self_config.club_id)
+ result = []
+ for record in records:
+ partner_config = ClubConfig.objects.filter(
+ club_id=record.partner_club_id,
+ is_self=0
+ ).first()
+ result.append({
+ 'club_id': record.partner_club_id,
+ 'club_nickname': partner_config.club_nickname if partner_config else record.partner_club_id,
+ 'club_avatar': partner_config.club_avatar if partner_config else '',
+ })
+ return Response({'code': 0, 'data': result})
+ except Exception as e:
+ logger.error(f"查询合作俱乐部失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+# ========== 2. 获取统计数据接口(完整) ==========
+
+
+
+
+
+class GetStatsView(APIView):
+ """
+ 获取派单统计数据(按日或按月)
+ 请求:POST /houtai/hqpfsj
+ 参数:{
+ "username": "客服手机号",
+ "club_id": "对方俱乐部ID",
+ "direction": 1, # 1=我方派单,2=对方派单
+ "granularity": "day", # "day" 或 "month"
+ # 趋势图参数(可选,不传则使用默认值):
+ "trend_year_month": "YYYY-MM", # 按日时表示月份,缺省当前月
+ "trend_year": "YYYY", # 按月时表示年份,缺省当前年
+ # KPI参数(可选,不传则使用默认值):
+ "date": "YYYY-MM-DD", # 按日时表示日期,缺省当天
+ "year_month": "YYYY-MM" # 按月时表示月份,缺省当前月
+ }
+ 权限:权限码2
+ 返回:{
+ "code": 0,
+ "data": {
+ "summary": { ... },
+ "chart": { ... }
+ }
+ }
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取基础参数
+ username = request.data.get('username', '').strip()
+ club_id = request.data.get('club_id', '').strip()
+ direction = request.data.get('direction')
+ granularity = request.data.get('granularity')
+
+ # 基础参数校验
+ if not username:
+ return Response({'code': 400, 'msg': 'username 不能为空'})
+ if not club_id:
+ return Response({'code': 400, 'msg': 'club_id 不能为空'})
+ if direction is None:
+ return Response({'code': 400, 'msg': 'direction 不能为空'})
+ if granularity not in ('day', 'month'):
+ return Response({'code': 400, 'msg': 'granularity 必须是 day 或 month'})
+ try:
+ direction = int(direction)
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': 'direction 必须为整数'})
+
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '004bb' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
+
+ # 4. 获取当前时间用于默认值
+ now = datetime.now()
+
+ # 5. 处理趋势图参数(默认当前月/年)
+ if granularity == 'day':
+ trend_year_month = request.data.get('trend_year_month')
+ if not trend_year_month:
+ trend_year_month = now.strftime('%Y-%m')
+ try:
+ year, month = map(int, trend_year_month.split('-'))
+ start_date = datetime(year, month, 1).date()
+ _, days_in_month = monthrange(year, month)
+ end_date = datetime(year, month, days_in_month).date()
+ except (ValueError, TypeError):
+ # 如果格式错误,回退到当前月
+ year, month = now.year, now.month
+ start_date = datetime(year, month, 1).date()
+ _, days_in_month = monthrange(year, month)
+ end_date = datetime(year, month, days_in_month).date()
+ else:
+ trend_year = request.data.get('trend_year')
+ if not trend_year:
+ trend_year = str(now.year)
+ try:
+ year = int(trend_year)
+ except (ValueError, TypeError):
+ year = now.year
+ start_date = datetime(year, 1, 1).date()
+ end_date = datetime(year, 12, 31).date()
+
+ # 6. 处理KPI参数(默认当天/当月)
+ if granularity == 'day':
+ date_str = request.data.get('date')
+ if not date_str:
+ date_str = now.strftime('%Y-%m-%d')
+ try:
+ target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
+ except (ValueError, TypeError):
+ target_date = now.date()
+ else:
+ year_month_str = request.data.get('year_month')
+ if not year_month_str:
+ year_month_str = now.strftime('%Y-%m')
+ try:
+ year, month = map(int, year_month_str.split('-'))
+ start_date_kpi = datetime(year, month, 1).date()
+ _, days_in_month = monthrange(year, month)
+ end_date_kpi = datetime(year, month, days_in_month).date()
+ except (ValueError, TypeError):
+ year, month = now.year, now.month
+ start_date_kpi = datetime(year, month, 1).date()
+ _, days_in_month = monthrange(year, month)
+ end_date_kpi = datetime(year, month, days_in_month).date()
+
+ # 7. 查询趋势图数据
+ response_data = {
+ 'summary': {'dispatch_count': 0, 'dispatch_amount': 0.0, 'success_count': 0, 'success_amount': 0.0},
+ 'chart': {'labels': [], 'dispatch_counts': [], 'dispatch_amounts': [], 'success_counts': [], 'success_amounts': []}
+ }
+
+ try:
+ if granularity == 'day':
+ qs = DailyDispatchStat.objects.filter(
+ partner_club_id=club_id,
+ direction=direction,
+ date__gte=start_date,
+ date__lte=end_date
+ ).order_by('date')
+ data_dict = {}
+ for item in qs:
+ data_dict[item.date] = {
+ 'dispatch_count': item.dispatch_count,
+ 'dispatch_amount': float(item.dispatch_amount),
+ 'success_count': item.success_count,
+ 'success_amount': float(item.success_amount),
+ }
+ labels = []
+ dispatch_counts = []
+ dispatch_amounts = []
+ success_counts = []
+ success_amounts = []
+ current = start_date
+ while current <= end_date:
+ labels.append(current.strftime('%d'))
+ day_data = data_dict.get(current, {})
+ dispatch_counts.append(day_data.get('dispatch_count', 0))
+ dispatch_amounts.append(day_data.get('dispatch_amount', 0.0))
+ success_counts.append(day_data.get('success_count', 0))
+ success_amounts.append(day_data.get('success_amount', 0.0))
+ current += timedelta(days=1)
+ response_data['chart'] = {
+ 'labels': labels,
+ 'dispatch_counts': dispatch_counts,
+ 'dispatch_amounts': dispatch_amounts,
+ 'success_counts': success_counts,
+ 'success_amounts': success_amounts,
+ }
+ else: # month
+ qs = DailyDispatchStat.objects.filter(
+ partner_club_id=club_id,
+ direction=direction,
+ date__gte=start_date,
+ date__lte=end_date
+ ).order_by('date')
+ month_totals = {m: {'dispatch_count': 0, 'dispatch_amount': 0.0, 'success_count': 0, 'success_amount': 0.0}
+ for m in range(1, 13)}
+ for item in qs:
+ m = item.date.month
+ month_totals[m]['dispatch_count'] += item.dispatch_count
+ month_totals[m]['dispatch_amount'] += float(item.dispatch_amount)
+ month_totals[m]['success_count'] += item.success_count
+ month_totals[m]['success_amount'] += float(item.success_amount)
+ labels = []
+ dispatch_counts = []
+ dispatch_amounts = []
+ success_counts = []
+ success_amounts = []
+ for m in range(1, 13):
+ labels.append(f"{m}月")
+ dispatch_counts.append(month_totals[m]['dispatch_count'])
+ dispatch_amounts.append(month_totals[m]['dispatch_amount'])
+ success_counts.append(month_totals[m]['success_count'])
+ success_amounts.append(month_totals[m]['success_amount'])
+ response_data['chart'] = {
+ 'labels': labels,
+ 'dispatch_counts': dispatch_counts,
+ 'dispatch_amounts': dispatch_amounts,
+ 'success_counts': success_counts,
+ 'success_amounts': success_amounts,
+ }
+
+ # 8. 查询KPI数据
+ if granularity == 'day':
+ try:
+ stat = DailyDispatchStat.objects.get(
+ partner_club_id=club_id,
+ direction=direction,
+ date=target_date
+ )
+ response_data['summary'] = {
+ 'dispatch_count': stat.dispatch_count,
+ 'dispatch_amount': float(stat.dispatch_amount),
+ 'success_count': stat.success_count,
+ 'success_amount': float(stat.success_amount),
+ }
+ except DailyDispatchStat.DoesNotExist:
+ response_data['summary'] = {
+ 'dispatch_count': 0,
+ 'dispatch_amount': 0.0,
+ 'success_count': 0,
+ 'success_amount': 0.0,
+ }
+ else: # month
+ qs_kpi = DailyDispatchStat.objects.filter(
+ partner_club_id=club_id,
+ direction=direction,
+ date__gte=start_date_kpi,
+ date__lte=end_date_kpi
+ )
+ summary = qs_kpi.aggregate(
+ dispatch_count=Sum('dispatch_count'),
+ dispatch_amount=Sum('dispatch_amount'),
+ success_count=Sum('success_count'),
+ success_amount=Sum('success_amount')
+ )
+ response_data['summary'] = {
+ 'dispatch_count': summary['dispatch_count'] or 0,
+ 'dispatch_amount': float(summary['dispatch_amount'] or 0.0),
+ 'success_count': summary['success_count'] or 0,
+ 'success_amount': float(summary['success_amount'] or 0.0),
+ }
+
+ return Response({'code': 0, 'data': response_data})
+
+ except Exception as e:
+ logger.error(f"获取统计数据失败: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+
+
+
+class PunishDashouView(APIView):
+ """
+ 客服处罚打手接口
+ 请求:POST /houtai/kpcf
+ 参数:{
+ "username": "客服手机号",
+ "dingdan_id": "订单ID",
+ "reason": "处罚原因(可选)"
+ }
+ 认证:JWT,权限码1
+ 返回:code=0 + 处罚结果
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ start_time = time.time()
+
+ # 1. 获取参数
+ username = request.data.get('username', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ reason = request.data.get('reason', '').strip()
+
+ if not username or not dingdan_id:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ try:
+ # 2. 身份验证
+ current_user = request.user
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '003aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权处罚'})
+
+ # 4. 查询订单并加锁
+ with transaction.atomic():
+ order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
+
+ # 5. 订单状态校验(允许处罚的状态:1,2,3,4,5,6,7,8,即除已完成以外的状态?这里宽松处理)
+ if order.zhuangtai not in [1,2,3,4,5,6,7,8]:
+ return Response({'code': 400, 'msg': f'订单状态 {order.zhuangtai} 不允许处罚'})
+
+ # 6. 判断是否已处罚(避免重复)
+ existing_punish = Chufajilu.objects.filter(
+ dingdan_id=dingdan_id,
+ dashouid=order.jiedan_dashou_id
+ ).exists()
+ if existing_punish:
+ return Response({'code': 400, 'msg': '该打手已被处罚过'})
+
+ # 7. 区分本地处罚还是通知对方平台
+ is_cross = order.is_cross
+ dispatch_type = order.dispatch_type
+ partner_club_id = order.partner_club_id
+ partner_order_id = order.partner_order_id
+ dashou_id = order.jiedan_dashou_id
+
+ # 如果没有接单打手ID,无法处罚
+ if not dashou_id:
+ return Response({'code': 400, 'msg': '订单无接单打手,无法处罚'})
+
+ # 本地处罚(处罚我方打手)
+ def local_punish():
+ # 扣除打手积分(假设每次扣5分)
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ dashou_profile.jifen -= 5
+ dashou_profile.save()
+
+ # 创建处罚记录
+ Chufajilu.objects.create(
+ dingdan_id=dingdan_id,
+ dashouid=dashou_id,
+ qingqiuid='', # 请求处罚ID,可空
+ chuliid=username,
+ cfliyou=reason or '',
+ sqzhuangtai=1, # 成功
+ bhliyou='',
+ ssliyou='',
+ jifen=5,
+ )
+ logger.info(f"本地处罚打手成功: 订单 {dingdan_id}, 打手 {dashou_id}, 扣分5")
+
+ # 情况A:对方派单(dispatch_type=2)或 本地订单
+ if dispatch_type == 2 or not is_cross:
+ # 直接本地处罚
+ local_punish()
+ return Response({'code': 0, 'msg': '处罚成功'})
+
+ # 情况B:我方派单且有对方信息(需要通知对方平台)
+ elif dispatch_type == 1 and partner_club_id and partner_order_id:
+ # 需要通知对方平台处罚(对方平台打手)
+ # 获取我方俱乐部ID
+ my_club_config = ClubConfig.objects.filter(is_self=1).first()
+ if not my_club_config:
+ logger.error("未找到我方俱乐部配置")
+ return Response({'code': 500, 'msg': '系统配置错误'})
+ self_club_id = my_club_config.club_id
+
+ # 获取对方服务器域名
+ try:
+ club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
+ partner_domain = club_rel.partner_domain
+ except ObjectDoesNotExist:
+ logger.error(f"未找到合作关系: {self_club_id} <-> {partner_club_id}")
+ return Response({'code': 500, 'msg': '未找到对方平台配置'})
+
+ # 调用对方平台处罚接口,最多重试2次
+ url = partner_domain.rstrip('/') + '/houtai/kptcfds'
+ payload = {
+ 'order_id': partner_order_id, # 对方平台的订单ID
+ 'club_id': self_club_id,
+ 'reason': reason,
+ 'operator': username
+ }
+ timeout = 3
+ retries = 2
+ success = False
+ for attempt in range(retries):
+ try:
+ resp = requests.post(url, json=payload, timeout=timeout)
+ if resp.status_code == 200:
+ resp_json = resp.json()
+ if resp_json.get('code') == 0:
+ success = True
+ logger.info(f"通知对方平台处罚成功: {dingdan_id}")
+ # 可选:在本平台记录处罚记录(标记已通知对方,但不扣分)
+ Chufajilu.objects.create(
+ dingdan_id=dingdan_id,
+ dashouid=dashou_id,
+ qingqiuid='',
+ chuliid=username,
+ cfliyou=reason or '',
+ sqzhuangtai=1,
+ bhliyou='',
+ ssliyou='',
+ jifen=0, # 对方扣分,本方不扣
+ )
+ break
+ else:
+ logger.warning(f"对方平台返回错误 (尝试{attempt+1}): {resp_json.get('msg')}")
+ else:
+ logger.warning(f"对方平台HTTP错误 (尝试{attempt+1}): {resp.status_code}")
+ except requests.exceptions.Timeout:
+ logger.warning(f"请求对方平台超时 (尝试{attempt+1}): {url}")
+ except Exception as e:
+ logger.warning(f"请求对方平台异常 (尝试{attempt+1}): {str(e)}")
+ if not success:
+ return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'})
+
+ return Response({'code': 0, 'msg': '处罚成功'})
+
+ else:
+ # 其他情况(例如我方派单但缺少对方信息),视为本地订单,处罚我方打手
+ local_punish()
+ return Response({'code': 0, 'msg': '处罚成功'})
+
+ except Dingdan.DoesNotExist:
+ logger.warning(f"订单不存在: {dingdan_id}")
+ return Response({'code': 404, 'msg': '订单不存在'})
+ except Exception as e:
+ logger.error(f"处罚接口异常: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+
+
+
+
+
+@method_decorator(csrf_exempt, name='dispatch')
+class PartnerPunishNotifyView(APIView):
+ """
+ 接收对方平台处罚通知接口
+ """
+ authentication_classes = []
+ permission_classes = [AllowAny]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # token = request.data.get('token')
+ # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN):
+ # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200)
+ try:
+ order_id = request.data.get('order_id', '').strip()
+ club_id = request.data.get('club_id', '').strip()
+ reason = request.data.get('reason', '').strip()
+ operator = request.data.get('operator', '').strip()
+
+ if not order_id or not club_id:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 事务开始
+ with transaction.atomic():
+ # 查询订单并加锁
+ try:
+ order = Dingdan.objects.select_for_update().get(dingdan_id=order_id)
+ except Dingdan.DoesNotExist:
+ logger.warning(f"处罚通知:订单不存在 {order_id}")
+ return Response({'code': 404, 'msg': '订单不存在'})
+
+ dashou_id = order.jiedan_dashou_id
+ if not dashou_id:
+ logger.warning(f"订单 {order_id} 无接单打手,无法处罚")
+ return Response({'code': 400, 'msg': '订单无接单打手'})
+
+ # 检查是否已处罚
+ existing = Chufajilu.objects.filter(
+ dingdan_id=order_id,
+ dashouid=dashou_id
+ ).exists()
+ if existing:
+ logger.info(f"订单 {order_id} 打手 {dashou_id} 已被处罚过,跳过")
+ return Response({'code': 0, 'msg': 'already punished'})
+
+ # 扣除积分
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ dashou_profile.jifen -= 5
+ dashou_profile.save()
+
+ # 创建处罚记录
+ Chufajilu.objects.create(
+ dingdan_id=order_id,
+ dashouid=dashou_id,
+ qingqiuid='',
+ chuliid=operator,
+ cfliyou=reason or '',
+ sqzhuangtai=0,
+ bhliyou='',
+ ssliyou='',
+ jifen=5,
+ )
+ logger.info(f"对方平台处罚成功: 订单 {order_id}, 打手 {dashou_id}")
+
+ return Response({'code': 0, 'msg': 'success'})
+
+ except Exception as e:
+ logger.error(f"对方处罚通知接口异常: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+
+
+
+
+
+
+
+# ==================== 接口1:获取俱乐部列表(权限码2) ====================
+class GetClubConfigForPreSettlementView(APIView):
+ """
+ 获取对方俱乐部配置列表(用于预结算页面)
+ 权限要求:客服身份 + 权限代码2且开启
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ user = request.user
+ # 1. 校验客服身份
+ try:
+ kefu = UserKefu.objects.select_related('user').get(user=user)
+ except ObjectDoesNotExist:
+ return Response({'code': 403, 'msg': '身份错误,非客服账号'})
+ if kefu.zhuangtai != 1:
+ return Response({'code': 403, 'msg': '客服账号已被禁用'})
+
+ # 2. 获取请求参数中的 username 并校验
+ data = request.data
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '004aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
+
+ # 4. 查询所有对方俱乐部(is_self=0)
+ club_configs = ClubConfig.objects.filter(is_self=0).only(
+ 'club_id', 'club_nickname', 'club_avatar', 'storage_bucket_domain'
+ )
+ data_list = []
+ for club in club_configs:
+ data_list.append({
+ 'club_id': club.club_id,
+ 'club_nickname': club.club_nickname or club.club_id,
+ 'club_avatar': club.club_avatar or '',
+ 'storage_bucket_domain': club.storage_bucket_domain or ''
+ })
+ return Response({'code': 0, 'msg': 'success', 'data': data_list})
+
+ except Exception as e:
+ logger.error(f"获取俱乐部配置失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+# ==================== 接口2:获取预结算订单列表 ====================
+class GetPreSettlementDataView(APIView):
+ """获取预结算订单列表(支持筛选、分页、统计)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ user = request.user
+ # 1. 客服身份校验
+ try:
+ kefu = UserKefu.objects.select_related('user').get(user=user)
+ except ObjectDoesNotExist:
+ return Response({'code': 403, 'msg': '身份错误,非客服账号'})
+ if kefu.zhuangtai != 1:
+ return Response({'code': 403, 'msg': '客服账号已被禁用'})
+
+ data = request.data
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限(003aa)
+ if '004aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
+
+ # 2. 获取筛选参数
+ status_filter = data.get('status_filter') # 0=待处理, 1=已处理(包含已拒绝)
+ club_id = data.get('club_id')
+ dingdan_id = data.get('dingdan_id')
+ partner_order_id = data.get('partner_order_id')
+ dashou_id = data.get('dashou_id')
+ clkf = data.get('clkf')
+ date_str = data.get('date') # 创建日期 YYYY-MM-DD
+ page = int(data.get('page', 1))
+ page_size = int(data.get('pageSize', 20))
+
+ # 3. 构建查询集
+ queryset = PreSettlement.objects.all()
+
+ # 状态筛选:0待处理,1已处理(status=1或2)
+ if status_filter == 0:
+ queryset = queryset.filter(status=0)
+ elif status_filter == 1:
+ queryset = queryset.filter(status__in=[1, 2])
+ # 如果status_filter为其他值(如'all'),则不筛选
+
+ if club_id:
+ queryset = queryset.filter(partner_club_id=club_id)
+ if dingdan_id:
+ queryset = queryset.filter(dingdan_id__icontains=dingdan_id)
+ if partner_order_id:
+ queryset = queryset.filter(partner_order_id__icontains=partner_order_id)
+ if dashou_id:
+ queryset = queryset.filter(dashou_id__icontains=dashou_id)
+ if clkf:
+ queryset = queryset.filter(operator__icontains=clkf)
+ if date_str:
+ try:
+ target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
+ queryset = queryset.filter(create_time__date=target_date)
+ except ValueError:
+ pass
+
+ # 4. 统计数据(待处理总数量和总金额、已处理总数量)
+ pending_queryset = PreSettlement.objects.filter(status=0)
+ # 应用相同的筛选条件(除了状态)
+ if club_id:
+ pending_queryset = pending_queryset.filter(partner_club_id=club_id)
+ if dingdan_id:
+ pending_queryset = pending_queryset.filter(dingdan_id__icontains=dingdan_id)
+ if partner_order_id:
+ pending_queryset = pending_queryset.filter(partner_order_id__icontains=partner_order_id)
+ if dashou_id:
+ pending_queryset = pending_queryset.filter(dashou_id__icontains=dashou_id)
+ if clkf:
+ pending_queryset = pending_queryset.filter(operator__icontains=clkf)
+ if date_str:
+ pending_queryset = pending_queryset.filter(create_time__date=target_date)
+
+ pending_count = pending_queryset.count()
+ pending_amount = pending_queryset.aggregate(total=Sum('order_amount'))['total'] or Decimal('0.00')
+
+ processed_count = PreSettlement.objects.filter(status__in=[1, 2])
+ if club_id:
+ processed_count = processed_count.filter(partner_club_id=club_id)
+ if dingdan_id:
+ processed_count = processed_count.filter(dingdan_id__icontains=dingdan_id)
+ if partner_order_id:
+ processed_count = processed_count.filter(partner_order_id__icontains=partner_order_id)
+ if dashou_id:
+ processed_count = processed_count.filter(dashou_id__icontains=dashou_id)
+ if clkf:
+ processed_count = processed_count.filter(operator__icontains=clkf)
+ if date_str:
+ processed_count = processed_count.filter(create_time__date=target_date)
+ processed_count = processed_count.count()
+
+ # 5. 分页
+ total = queryset.count()
+ start = (page - 1) * page_size
+ end = start + page_size
+ items = queryset.order_by('-create_time')[start:end]
+
+ # 6. 构造返回数据
+ list_data = []
+ for item in items:
+ list_data.append({
+ 'dingdan_id': item.dingdan_id,
+ 'partner_order_id': item.partner_order_id,
+ 'partner_club_id': item.partner_club_id,
+ 'dashou_id': item.dashou_id,
+ 'amount': float(item.amount),
+ 'order_amount': float(item.order_amount),
+ 'operator': item.operator,
+ 'status': item.status,
+ 'status_text': dict(PreSettlement.STATUS_CHOICES).get(item.status, '未知'),
+ 'create_time': item.create_time.strftime('%Y-%m-%d %H:%M:%S') if item.create_time else '',
+ 'process_time': item.process_time.strftime('%Y-%m-%d %H:%M:%S') if item.process_time else '',
+ })
+
+ return Response({
+ 'code': 0,
+ 'msg': 'success',
+ 'data': {
+ 'list': list_data,
+ 'total': total,
+ 'stats': {
+ 'pending_count': pending_count,
+ 'pending_amount': float(pending_amount),
+ 'processed_count': processed_count
+ }
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"获取预结算数据失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+# ==================== 接口3:打款/拒绝操作 ====================
+class PreSettlementActionView(APIView):
+ """预结算打款/拒绝操作(原子事务,保证资金安全)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ user = request.user
+ # 1. 客服身份校验
+ try:
+ kefu = UserKefu.objects.select_related('user').get(user=user)
+ except ObjectDoesNotExist:
+ return Response({'code': 403, 'msg': '身份错误,非客服账号'})
+ if kefu.zhuangtai != 1:
+ return Response({'code': 403, 'msg': '客服账号已被禁用'})
+
+ data = request.data
+
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否有跨平台管理权限()
+ if '004aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权处理预结算打款'})
+
+ dingdan_id = data.get('dingdan_id')
+ action = data.get('action') # 'pay' 或 'reject'
+
+ if not dingdan_id or action not in ['pay', 'reject']:
+ return Response({'code': 400, 'msg': '参数错误'})
+
+ with transaction.atomic():
+ # 锁定预结算记录(行锁)
+ pre_settlement = PreSettlement.objects.select_for_update().get(dingdan_id=dingdan_id)
+
+ # 只有状态为0(待处理)才能操作
+ if pre_settlement.status != 0:
+ return Response({'code': 400, 'msg': '该记录已处理,不可重复操作'})
+
+ if action == 'pay':
+ # 打款操作:增加打手余额及相关统计
+ dashou_id = pre_settlement.dashou_id
+ amount = pre_settlement.amount
+
+ # 查询打手扩展表
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ return Response({'code': 404, 'msg': '打手不存在'})
+
+ # 使用 F 表达式原子更新余额和统计字段(防止并发覆盖)
+ dashou_profile.yue = F('yue') + amount
+ dashou_profile.zonge = F('zonge') + amount
+ dashou_profile.jinrishouyi = F('jinrishouyi') + amount
+ dashou_profile.jinyueshouyi = F('jinyueshouyi') + amount
+ dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1
+ dashou_profile.save(update_fields=['yue', 'zonge', 'jinrishouyi', 'jinyueshouyi', 'chengjiaozongliang'])
+
+ # 更新预结算状态为已结算
+ pre_settlement.status = 1
+ pre_settlement.process_time = timezone.now()
+ pre_settlement.operator = user.phone
+ pre_settlement.save()
+
+ # ---------- 打手每日统计(成交) ----------
+
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=dashou_id,
+ amount=amount,
+ action=2 # 2 表示成交(结算)
+ )
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+
+
+ logger.info(f"预结算打款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {amount}")
+
+ logger.info(f"预结算打款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {amount}")
+
+ elif action == 'reject':
+ # 拒绝操作:仅修改状态,不涉及资金变动
+ pre_settlement.status = 2
+ pre_settlement.process_time = timezone.now()
+ pre_settlement.operator = user.phone
+ pre_settlement.save()
+ logger.info(f"预结算拒绝: 订单 {dingdan_id}")
+
+ # ---------- 打手每日统计(成交) ----------
+ dashou_id = pre_settlement.dashou_id
+ amount = pre_settlement.amount
+ try:
+ dashou_id = pre_settlement.dashou_id
+ amount = pre_settlement.amount
+ update_dashou_daily_by_action(
+ yonghuid=dashou_id,
+ amount=amount,
+ action=3 # 3 表示退款(退款)
+ )
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ logger.info(f"预结算拒绝: 订单 {dingdan_id}")
+
+ return Response({'code': 0, 'msg': '操作成功'})
+
+ except PreSettlement.DoesNotExist:
+ return Response({'code': 404, 'msg': '预结算记录不存在'})
+ except Exception as e:
+ logger.error(f"预结算操作异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+
+
+
+
+
+class GetRolePermissionView(APIView):
+ """
+ 获取所有角色、权限以及角色关联的用户数
+ 权限要求:用户必须拥有 000001 权限
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 1. 权限校验(验证客服身份,并获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 2. 检查是否有 000001 权限
+ if '000001' not in permissions:
+ return Response({'code': 403, 'msg': '您无权访问此页面'})
+
+ # 3. 获取所有权限
+ all_perms = Permission.objects.all().values('perm_code', 'perm_name', 'description')
+ permissions_list = list(all_perms)
+
+ # 4. 获取所有角色,并统计每个角色下的用户数和关联的权限
+ roles = Role.objects.all()
+ roles_data = []
+ for role in roles:
+ # 统计该角色下的用户数
+ user_count = UserRole.objects.filter(role=role).count()
+ # 获取该角色绑定的权限
+ role_perms = RolePermission.objects.filter(role=role).select_related('permission')
+ perms_of_role = [
+ {
+ 'perm_code': rp.permission.perm_code,
+ 'perm_name': rp.permission.perm_name,
+ 'description': rp.permission.description
+ }
+ for rp in role_perms
+ ]
+ roles_data.append({
+ 'role_code': role.role_code,
+ 'role_name': role.role_name,
+ 'description': role.description,
+ 'user_count': user_count,
+ 'permissions': perms_of_role
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'roles': roles_data,
+ 'permissions': permissions_list
+ }
+ })
+
+
+class ModifyRolePermissionView(APIView):
+ """
+ 通用接口:支持删除角色、为角色添加权限、移除角色权限
+ 操作类型通过 action 字段区分
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ action = request.data.get('action') # delete_role, add_permission, remove_permission
+ if not username or not action:
+ return Response({'code': 400, 'msg': '缺少必要参数'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '000001' not in permissions:
+ return Response({'code': 403, 'msg': '您无权进行此操作'})
+
+ # 获取角色(需要角色编码)
+ role_code = request.data.get('role_code')
+ if not role_code:
+ return Response({'code': 400, 'msg': '缺少角色编码'})
+
+ try:
+ role = Role.objects.get(role_code=role_code)
+ except Role.DoesNotExist:
+ return Response({'code': 404, 'msg': '角色不存在'})
+
+ # 超级管理员角色(role_code='000001')的特殊限制
+ if role.role_code == '000001':
+ if action in ['delete_role']:
+ return Response({'code': 403, 'msg': '超级管理员角色不能删除'})
+ # 修改名称/描述(本接口不涉及名称描述修改,但若后续扩展需限制)
+
+ # 根据 action 处理
+ if action == 'delete_role':
+ # 删除角色:同时删除角色权限关联、用户角色关联
+ with transaction.atomic():
+ RolePermission.objects.filter(role=role).delete()
+ UserRole.objects.filter(role=role).delete()
+ role.delete()
+ return Response({'code': 0, 'msg': '角色删除成功'})
+
+ elif action == 'add_permission':
+ perm_code = request.data.get('perm_code')
+ if not perm_code:
+ return Response({'code': 400, 'msg': '缺少权限编码'})
+ # 超级管理员角色不能添加 000001 权限(但实际它已拥有,这里防御)
+ if role.role_code == '000001' and perm_code == '000001':
+ return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'})
+ try:
+ perm = Permission.objects.get(perm_code=perm_code)
+ except Permission.DoesNotExist:
+ return Response({'code': 404, 'msg': '权限不存在'})
+ # 检查是否已存在
+ if RolePermission.objects.filter(role=role, permission=perm).exists():
+ return Response({'code': 400, 'msg': '该角色已拥有此权限'})
+ RolePermission.objects.create(role=role, permission=perm)
+ return Response({'code': 0, 'msg': '权限添加成功'})
+
+ elif action == 'remove_permission':
+ perm_code = request.data.get('perm_code')
+ if not perm_code:
+ return Response({'code': 400, 'msg': '缺少权限编码'})
+ # 超级管理员角色不能删除 000001 权限
+ if role.role_code == '000001' and perm_code == '000001':
+ return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'})
+ try:
+ perm = Permission.objects.get(perm_code=perm_code)
+ except Permission.DoesNotExist:
+ return Response({'code': 404, 'msg': '权限不存在'})
+ deleted = RolePermission.objects.filter(role=role, permission=perm).delete()
+ if deleted[0] == 0:
+ return Response({'code': 400, 'msg': '该角色未拥有此权限'})
+ return Response({'code': 0, 'msg': '权限移除成功'})
+
+ else:
+ return Response({'code': 400, 'msg': '无效的 action'})
+
+
+class AddRoleView(APIView):
+ """
+ 添加角色:自动生成角色编码,前端提供角色名称、描述、选择的权限列表
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ role_name = request.data.get('role_name', '').strip()
+ description = request.data.get('description', '').strip()
+ perm_codes = request.data.get('perm_codes', []) # 列表
+
+ if not username or not role_name:
+ return Response({'code': 400, 'msg': '缺少必要参数'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '000001' not in permissions:
+ return Response({'code': 403, 'msg': '您无权进行此操作'})
+
+ # 检查角色名称是否已存在
+ if Role.objects.filter(role_name=role_name).exists():
+ return Response({'code': 400, 'msg': '角色名称已存在'})
+
+ # 生成唯一角色编码(格式:ROLE_ + 8位随机字母数字)
+ while True:
+ random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
+ role_code = f"ROLE_{random_str}"
+ if not Role.objects.filter(role_code=role_code).exists():
+ break
+
+ # 创建角色
+ with transaction.atomic():
+ role = Role.objects.create(
+ role_code=role_code,
+ role_name=role_name,
+ description=description,
+
+ )
+ # 绑定权限(过滤掉不存在的权限编码)
+ for pc in perm_codes:
+ # 不允许添加 000001 权限(此权限仅供超级管理员)
+ if pc == '000001':
+ continue
+ try:
+ perm = Permission.objects.get(perm_code=pc)
+ RolePermission.objects.create(role=role, permission=perm)
+ except Permission.DoesNotExist:
+ # 忽略不存在的权限,或记录日志
+ logger.warning(f"添加角色时忽略不存在的权限编码: {pc}")
+ pass
+
+ return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}})
+
+
+
+class GetAdminUserListView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '000001' not in permissions:
+ return Response({'code': 403, 'msg': '您无权访问此页面'})
+
+ phone = request.data.get('phone', '').strip()
+ nicheng = request.data.get('nicheng', '').strip()
+ role_code = request.data.get('roleCode', '').strip()
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 20))
+
+ users = UserMain.objects.filter(
+ user_type='kefu',
+ kefu_profile__isnull=False
+ ).select_related('kefu_profile')
+
+ if phone:
+ users = users.filter(phone__icontains=phone)
+ if nicheng:
+ users = users.filter(kefu_profile__nicheng__icontains=nicheng)
+
+ # 关键修正:先求值为列表,再传入 phone__in
+ if role_code:
+ user_ids_with_role = list(
+ UserRole.objects.filter(role__role_code=role_code)
+ .values_list('account_id', flat=True)
+ )
+ users = users.filter(phone__in=user_ids_with_role)
+
+ users = users.order_by('-create_time')
+ paginator = Paginator(users, page_size)
+ page_obj = paginator.get_page(page)
+
+ data_list = []
+ for user in page_obj:
+ user_roles = UserRole.objects.filter(account_id=user.phone).select_related('role')
+ roles_data = [
+ {'role_code': ur.role.role_code, 'role_name': ur.role.role_name}
+ for ur in user_roles
+ ]
+ data_list.append({
+ 'phone': user.phone,
+ 'nicheng': user.kefu_profile.nicheng,
+ 'roles': roles_data,
+ 'status': user.kefu_profile.zhuangtai,
+ 'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(user, 'create_time') else '',
+ 'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(user, 'update_time') else '',
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': data_list,
+ 'total': paginator.count,
+ 'page': page,
+ 'pageSize': page_size
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"GetAdminUserListView 异常: {e}\n{traceback.format_exc()}")
+ return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'})
+
+
+
+class GetAdminRolesView(APIView):
+ """获取所有角色(用于筛选和分配)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '000001' not in permissions:
+ return Response({'code': 403, 'msg': '您无权访问此页面'})
+
+ roles = Role.objects.all().values('role_code', 'role_name')
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'roles': list(roles)
+ }
+ })
+
+
+
+
+
+
+'''class GetAdminUserListView(APIView):
+ """获取后台用户列表(支持分页、筛选)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '000001' not in permissions:
+ return Response({'code': 403, 'msg': '您无权访问此页面'})
+
+ # 筛选条件
+ phone = request.data.get('phone', '').strip()
+ nicheng = request.data.get('nicheng', '').strip()
+ role_code = request.data.get('roleCode', '').strip()
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 20))
+
+ # 基础查询:user_type='kefu' 且存在客服扩展表
+ users = UserMain.objects.filter(user_type='kefu', kefu_profile__isnull=False).select_related('kefu_profile')
+ if phone:
+ users = users.filter(phone__icontains=phone)
+ if nicheng:
+ users = users.filter(kefu_profile__nicheng__icontains=nicheng)
+ if role_code:
+ # 筛选拥有该角色的用户
+ user_ids_with_role = UserRole.objects.filter(role__role_code=role_code).values_list('account_id', flat=True)
+ users = users.filter(phone__in=user_ids_with_role)
+
+ # 排序
+ users = users.order_by('-create_time')
+
+ # 分页
+ paginator = Paginator(users, page_size)
+ page_obj = paginator.get_page(page)
+
+ # 构建返回数据
+ data_list = []
+ for user in page_obj:
+ # 获取用户的所有角色
+ user_roles = UserRole.objects.filter(account_id=user.phone).select_related('role')
+ roles_data = [{'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles]
+ data_list.append({
+ 'phone': user.phone,
+ 'nicheng': user.kefu_profile.nicheng,
+ 'roles': roles_data,
+ 'status': user.kefu_profile.zhuangtai,
+ 'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S'),
+ 'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S'),
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': data_list,
+ 'total': paginator.count,
+ 'page': page,
+ 'pageSize': page_size
+ }
+ })'''
+
+
+
+
+
+class ModifyAdminUserView(APIView):
+ """修改后台用户信息(昵称、密码、二级密码、状态、角色)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ action = request.data.get('action')
+ if not username or not action:
+ return Response({'code': 400, 'msg': '缺少参数'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '000001' not in permissions:
+ return Response({'code': 403, 'msg': '您无权进行此操作'})
+
+ target_phone = request.data.get('phone', '').strip()
+ if not target_phone:
+ return Response({'code': 400, 'msg': '缺少目标用户账号'})
+
+ try:
+ target_user = UserMain.objects.get(phone=target_phone, user_type='kefu')
+ target_kefu = target_user.kefu_profile
+ except UserMain.DoesNotExist:
+ return Response({'code': 404, 'msg': '用户不存在'})
+ except UserKefu.DoesNotExist:
+ return Response({'code': 404, 'msg': '用户扩展信息缺失'})
+
+ with transaction.atomic():
+ if action == 'update_user_info':
+ # 修改昵称、状态、密码、二级密码
+ nicheng = request.data.get('nicheng')
+ status = request.data.get('status')
+ password = request.data.get('password')
+ erjimima = request.data.get('erjimima')
+ if nicheng is not None:
+ target_kefu.nicheng = nicheng
+ if status is not None:
+ target_kefu.zhuangtai = int(status)
+ if password:
+ if len(password) < 6:
+ return Response({'code': 400, 'msg': '密码至少6位'})
+ target_user.password = password
+ if erjimima:
+ if len(erjimima) < 6:
+ return Response({'code': 400, 'msg': '二级密码至少6位'})
+ target_kefu.erjimima = erjimima # 假设二级密码也需要加密
+ target_user.save()
+ target_kefu.save()
+ return Response({'code': 0, 'msg': '修改成功'})
+
+ elif action == 'add_user_roles':
+ role_codes = request.data.get('role_codes', [])
+ if not role_codes:
+ return Response({'code': 400, 'msg': '请选择角色'})
+ for rc in role_codes:
+ try:
+ role = Role.objects.get(role_code=rc)
+ except Role.DoesNotExist:
+ continue
+ UserRole.objects.get_or_create(account_id=target_phone, role=role)
+ return Response({'code': 0, 'msg': '角色添加成功'})
+
+ elif action == 'remove_user_role':
+ role_code = request.data.get('role_code')
+ if not role_code:
+ return Response({'code': 400, 'msg': '缺少角色编码'})
+ try:
+ role = Role.objects.get(role_code=role_code)
+ except Role.DoesNotExist:
+ return Response({'code': 404, 'msg': '角色不存在'})
+ UserRole.objects.filter(account_id=target_phone, role=role).delete()
+ return Response({'code': 0, 'msg': '角色移除成功'})
+
+ else:
+ return Response({'code': 400, 'msg': '无效的action'})
+
+
+
+
+
+class AddAdminUserView(APIView):
+ """添加后台用户(客服账号)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '000001' not in permissions:
+ return Response({'code': 403, 'msg': '您无权进行此操作'})
+
+ phone = request.data.get('phone', '').strip()
+ password = request.data.get('password', '').strip()
+ erjimima = request.data.get('erjimima', '').strip()
+ nicheng = request.data.get('nicheng', '').strip()
+ role_codes = request.data.get('role_codes', [])
+
+ if not all([phone, password, erjimima, nicheng]):
+ return Response({'code': 400, 'msg': '请填写完整信息'})
+ if len(password) < 6 or len(erjimima) < 6:
+ return Response({'code': 400, 'msg': '密码和二级密码至少6位'})
+
+ # 校验手机号是否已被占用(且 user_type='kefu')
+ if UserMain.objects.filter(phone=phone, user_type='kefu').exists():
+ return Response({'code': 400, 'msg': '该账号已存在'})
+
+ # 生成唯一的 yonghuid(7位数字)
+ while True:
+ yonghuid = str(random.randint(1000000, 9999999))
+ if not UserMain.objects.filter(yonghuid=yonghuid).exists():
+ break
+
+ # 生成唯一的 openid(格式:admin_{时间戳}_{随机6位})
+ timestamp = str(int(time.time() * 1000))
+ random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
+ openid = f"admin_{timestamp}_{random_suffix}"
+ while UserMain.objects.filter(openid=openid).exists():
+ random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
+ openid = f"admin_{timestamp}_{random_suffix}"
+
+ with transaction.atomic():
+ user_main = UserMain.objects.create(
+ yonghuid=yonghuid,
+ openid=openid,
+ phone=phone,
+ password=password,
+ user_type='kefu'
+ )
+ # 创建客服扩展表(后台用户扩展)
+ UserKefu.objects.create(
+ user=user_main,
+ nicheng=nicheng,
+ erjimima=erjimima,
+ zhuangtai=1
+ )
+ # 分配角色
+ for rc in role_codes:
+ try:
+ role = Role.objects.get(role_code=rc)
+ UserRole.objects.create(account_id=phone, role=role)
+ except Role.DoesNotExist:
+ pass
+
+ return Response({'code': 0, 'msg': '添加成功'})
+
+
+
+
+
+
+
+
+class KefuGetDashouListView(APIView):
+ """
+ 客服获取打手列表接口(仅打手类型)
+ 需要拥有以下任一权限:001aa,001bb,001cc,001dd,001ee,001ff,001gg
+ 请求:POST /yonghu/kefuhqdslb
+ 参数:
+ username: 客服手机号(用于权限校验)
+ keyword: 搜索关键词(可选,搜索用户ID或昵称)
+ page: 页码(从1开始)
+ page_size: 每页数量
+ 返回:
+ code: 0
+ data: {
+ list: [打手对象],
+ has_more: bool,
+ total: int, // 总打手数
+ valid_dashou_count: int // 有效打手数(有有效会员)
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数(前端字段为 username)
+ username = request.data.get('username', '').strip()
+ keyword = request.data.get('keyword', '').strip()
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 20))
+ except ValueError:
+ return Response({'code': 400, 'msg': '分页参数错误'})
+
+ if not username:
+ return Response({'code': 401, 'msg': '认证失败'})
+
+ # 2. 公共权限校验(验证客服身份,获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否拥有打手管理相关权限(至少一个)
+ required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'}
+ if not any(p in permissions for p in required_perms):
+ return Response({'code': 403, 'msg': '您没有权限查看打手列表'})
+
+ # 4. 构建打手查询集(仅打手类型)
+ dashou_qs = UserDashou.objects.select_related('user').all()
+
+ # 关键词筛选(用户ID或昵称)
+ if keyword:
+ dashou_qs = dashou_qs.filter(
+ Q(user__yonghuid__icontains=keyword) |
+ Q(nicheng__icontains=keyword)
+ )
+
+ # 5. 统计打手总数
+ total_dashou = dashou_qs.count()
+
+ # 6. 统计有效打手数(有会员且未过期)
+ dashou_user_ids = dashou_qs.values_list('user__yonghuid', flat=True)
+ now = timezone.now()
+ valid_user_ids = Huiyuangoumai.objects.filter(
+ yonghu_id__in=dashou_user_ids,
+ huiyuan_zhuangtai=1,
+ daoqi_time__gt=now
+ ).values_list('yonghu_id', flat=True).distinct()
+ valid_dashou_count = valid_user_ids.count()
+
+ # 7. 分页
+ offset = (page - 1) * page_size
+ dashou_list = dashou_qs.order_by('-create_time')[offset:offset + page_size]
+
+ # 8. 构建返回数据
+ result = []
+ for dashou in dashou_list:
+ user = dashou.user
+ result.append({
+ 'yonghuid': user.yonghuid,
+ 'avatar': user.avatar or '',
+ 'zaixianzhuangtai': dashou.zaixianzhuangtai,
+ 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
+ 'nicheng': dashou.nicheng or '',
+ 'zhuangtai': dashou.zhuangtai,
+ })
+
+ has_more = (offset + len(dashou_list)) < total_dashou
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': result,
+ 'has_more': has_more,
+ 'total': total_dashou,
+ 'valid_dashou_count': valid_dashou_count,
+ }
+ })
+
+
+
+class KefuGetDashouDetailView(APIView):
+ """
+ 客服获取打手详情接口
+ 请求:POST /yonghu/kefuhqdsxq
+ 参数:{
+ "username": "客服手机号",
+ "uid": "打手ID (yonghuid)"
+ }
+ 认证:JWT
+ 返回:code=0 + 打手信息 + 会员列表 + 所有会员类型 + 客服权限列表
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ username = request.data.get('phone', '').strip()
+ uid = request.data.get('uid', '').strip()
+
+ if not username or not uid:
+ return Response({'code': 400, 'msg': '缺少参数'})
+
+ # 2. 公共权限校验(验证客服身份,获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ # 3. 检查是否拥有打手管理相关权限(至少一个)
+ required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'}
+ if not any(p in permissions for p in required_perms):
+ return Response({'code': 403, 'msg': '您没有权限查看打手详情'})
+
+ # 4. 查询打手主表和扩展表
+ try:
+ user_main = UserMain.objects.select_related('dashou_profile').get(
+ yonghuid=uid
+ )
+ except UserMain.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手不存在'})
+
+ try:
+ dashou = user_main.dashou_profile
+ except ObjectDoesNotExist:
+ # 用户存在但未注册打手身份,返回基本信息
+ user_info = {
+ 'yonghuid': user_main.yonghuid,
+ 'avatar': user_main.avatar or '',
+ 'zaixianzhuangtai': 0,
+ 'zhanghaozhuangtai': 0,
+ 'zhuangtai': 0,
+ 'nicheng': '',
+ 'chenghao': '',
+ 'dianhua': '',
+ 'wechat': '',
+ 'phone': user_main.phone or '',
+ 'create_time': user_main.create_time,
+ 'ip': user_main.ip or '',
+ 'jiedanzongliang': 0,
+ 'chengjiaozongliang': 0,
+ 'tuikuanliang': 0,
+ 'jinrijiedan': 0,
+ 'yue': 0.00,
+ 'zonge': 0.00,
+ 'jifen': 0,
+ 'yajin': 0.00,
+ 'jinrishouyi': 0.00,
+ 'jinyueshouyi': 0.00,
+ 'yaoqingren': '',
+ 'jieshao': '',
+ }
+ member_list = []
+ # 仍然需要返回 all_members 和 permissions
+ all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao'))
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'user_info': user_info,
+ 'member_list': member_list,
+ 'all_members': all_members,
+ 'permissions': permissions,
+ }
+ })
+
+ # 5. 构建打手信息
+ user_info = {
+ 'yonghuid': user_main.yonghuid,
+ 'avatar': user_main.avatar or '',
+ 'zaixianzhuangtai': dashou.zaixianzhuangtai,
+ 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
+ 'zhuangtai': dashou.zhuangtai,
+ 'nicheng': dashou.nicheng or '',
+ 'chenghao': dashou.chenghao or '',
+ 'dianhua': dashou.dianhua or '',
+ 'wechat': dashou.wechat or '',
+ 'phone': user_main.phone or '',
+ 'create_time': user_main.create_time,
+ 'ip': user_main.ip or '',
+ 'jiedanzongliang': dashou.jiedanzongliang,
+ 'chengjiaozongliang': dashou.chengjiaozongliang,
+ 'tuikuanliang': dashou.tuikuanliang,
+ 'jinrijiedan': dashou.jinrijiedan,
+ 'yue': float(dashou.yue) if dashou.yue else 0.00,
+ 'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
+ 'jifen': dashou.jifen,
+ 'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
+ 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
+ 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
+ 'yaoqingren': dashou.yaoqingren or '',
+ 'jieshao': dashou.jieshao or '',
+ }
+
+ # 6. 查询打手已拥有的会员(未过期)
+ member_records = Huiyuangoumai.objects.filter(
+ yonghu_id=uid
+ ).order_by('-create_time')
+ member_list = []
+ for record in member_records:
+ # 检查是否过期
+ if hasattr(record, 'jiance_shifou_daoqi'):
+ is_expired = record.jiance_shifou_daoqi()
+ else:
+ is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
+
+ if not is_expired:
+ try:
+ huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id)
+ huiyuan_name = huiyuan.jieshao
+ except Huiyuan.DoesNotExist:
+ huiyuan_name = '未知会员'
+ member_list.append({
+ 'id': record.id,
+ 'huiyuan_name': huiyuan_name,
+ 'daoqi_time': record.daoqi_time,
+ 'is_active': record.huiyuan_zhuangtai == 1,
+ 'huiyuan_id': record.huiyuan_id, # 用于删除操作
+ })
+
+ # 7. 查询所有会员类型(用于前端添加会员)
+ all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao'))
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'user_info': user_info,
+ 'member_list': member_list,
+ 'all_members': all_members,
+ 'permissions': permissions, # 返回当前客服的权限列表
+ }
+ })
+
+
+
+
+'''class KefuGetDashouDetailView(APIView):
+ """
+ 客服获取打手详情接口
+ 请求:POST /yonghu/kefuhqdsxq
+ 参数:{
+ "phone": "客服手机号",
+ "uid": "打手ID (yonghuid)"
+ }
+ 认证:JWT
+ 返回:code=0 + 打手信息 + 会员列表
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ username = request.data.get('phone', '').strip()
+ uid = request.data.get('uid', '').strip()
+
+ if not username or not uid:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+
+ # 2. 公共权限校验(验证客服身份,获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查是否拥有打手管理相关权限(至少一个)
+ required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'}
+ if not any(p in permissions for p in required_perms):
+ return Response({'code': 403, 'msg': '您没有权限查看打手列表'})
+
+ # 3. 查询打手主表和扩展表
+ try:
+ # 使用 select_related 预加载 dashou_profile,减少数据库查询
+ user_main = UserMain.objects.select_related('dashou_profile').get(
+ yonghuid=uid
+ )
+ except UserMain.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 验证用户类型是否为打手(可选,但建议验证)
+ # 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册)
+ try:
+ dashou = user_main.dashou_profile
+ except ObjectDoesNotExist:
+ # 用户存在但未注册打手身份,返回基本信息
+ user_info = {
+ 'yonghuid': user_main.yonghuid,
+ 'avatar': user_main.avatar or '',
+ 'zaixianzhuangtai': 0,
+ 'zhanghaozhuangtai': 0,
+ 'zhuangtai': 0,
+ 'nicheng': '',
+ 'chenghao': '',
+ 'dianhua': '',
+ 'wechat': '',
+ 'phone': user_main.phone or '',
+ 'create_time': user_main.create_time,
+ 'ip': user_main.ip or '',
+ 'jiedanzongliang': 0,
+ 'chengjiaozongliang': 0,
+ 'tuikuanliang': 0,
+ 'jinrijiedan': 0,
+ 'yue': 0.00,
+ 'zonge': 0.00,
+ 'jifen': 0,
+ 'yajin': 0.00,
+ 'jinrishouyi': 0.00,
+ 'jinyueshouyi': 0.00,
+ 'yaoqingren': '',
+ 'jieshao': '',
+ }
+ member_list = []
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'user_info': user_info,
+ 'member_list': member_list
+ }
+ })
+
+ # 4. 构建打手信息
+ user_info = {
+ 'yonghuid': user_main.yonghuid,
+ 'avatar': user_main.avatar or '',
+ 'zaixianzhuangtai': dashou.zaixianzhuangtai,
+ 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
+ 'zhuangtai': dashou.zhuangtai,
+ 'nicheng': dashou.nicheng or '',
+ 'chenghao': dashou.chenghao or '',
+ 'dianhua': dashou.dianhua or '',
+ 'wechat': dashou.wechat or '',
+ 'phone': user_main.phone or '',
+ 'create_time': user_main.create_time,
+ 'ip': user_main.ip or '',
+ 'jiedanzongliang': dashou.jiedanzongliang,
+ 'chengjiaozongliang': dashou.chengjiaozongliang,
+ 'tuikuanliang': dashou.tuikuanliang,
+ 'jinrijiedan': dashou.jinrijiedan,
+ 'yue': float(dashou.yue) if dashou.yue else 0.00,
+ 'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
+ 'jifen': dashou.jifen,
+ 'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
+ 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
+ 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
+ 'yaoqingren': dashou.yaoqingren or '',
+ 'jieshao': dashou.jieshao or '',
+ }
+
+ # 5. 查询打手会员信息(仅未过期)
+ member_records = Huiyuangoumai.objects.filter(
+ yonghu_id=uid
+ ).order_by('-create_time')
+
+ member_list = []
+ for record in member_records:
+ # 调用模型方法检查是否过期
+ if hasattr(record, 'jiance_shifou_daoqi'):
+ is_expired = record.jiance_shifou_daoqi()
+ else:
+ # 如果方法不存在,简单判断
+ is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
+
+ if not is_expired:
+ # 获取会员详情
+ try:
+ huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id)
+ huiyuan_name = huiyuan.jieshao
+ except Huiyuan.DoesNotExist:
+ huiyuan_name = '未知会员'
+
+ member_list.append({
+ 'id': record.id,
+ 'huiyuan_name': huiyuan_name,
+ 'daoqi_time': record.daoqi_time,
+ 'is_active': record.huiyuan_zhuangtai == 1,
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'user_info': user_info,
+ 'member_list': member_list
+ }
+ })'''
+
+
+
+
+
+
+class KefuUpdateDashouView(APIView):
+ """
+ 客服修改打手信息接口
+ 支持操作:
+ - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb)
+ - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb)
+ - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd)
+ - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee)
+ - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee)
+ - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee)
+ - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg)
+ - gai_chenghao : 修改打手称号(升级/降级金牌) (权限001hh)
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ dashou_id = request.data.get('dashou_id', '').strip()
+ caozuo = request.data.get('caozuo', '').strip()
+ value = request.data.get('value') # 金额、积分、天数、称号等
+ huiyuan_id = request.data.get('huiyuan_id', '').strip()
+
+ if not username or not dashou_id or not caozuo:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ # 3. 查询打手
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ except UserMain.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手不存在'})
+ except UserDashou.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手扩展信息缺失'})
+
+ with transaction.atomic():
+ try:
+ before = {
+ 'yue': dashou_profile.yue,
+ 'yajin': dashou_profile.yajin,
+ 'jifen': dashou_profile.jifen,
+ 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'zhuangtai': dashou_profile.zhuangtai,
+ 'chenghao': dashou_profile.chenghao,
+ }
+
+ # ---------- 余额操作 ----------
+ if caozuo == 'jian_yue':
+ if '001aa' not in permissions:
+ return Response({'code': 403, 'msg': '无权限减打手余额'})
+ amount = Decimal(str(value))
+ if dashou_profile.yue < amount:
+ return Response({'code': 400, 'msg': '余额不足'})
+ dashou_profile.yue -= amount
+ dashou_profile.save()
+ after = dashou_profile.yue
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ xiugaitijiao=after,
+ xiugaitijiaoq=before['yue'],
+ )
+ return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after)}})
+
+ elif caozuo == 'jia_yue':
+ if '001bb' not in permissions:
+ return Response({'code': 403, 'msg': '无权限加打手余额'})
+ amount = Decimal(str(value))
+ dashou_profile.yue += amount
+ dashou_profile.save()
+ after = dashou_profile.yue
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ xiugaitijiao=after,
+ xiugaitijiaoq=before['yue'],
+ )
+ return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after)}})
+
+ # ---------- 押金操作 ----------
+ elif caozuo == 'jian_yajin':
+ if '001aa' not in permissions:
+ return Response({'code': 403, 'msg': '无权限减打手押金'})
+ amount = Decimal(str(value))
+ if dashou_profile.yajin < amount:
+ return Response({'code': 400, 'msg': '押金不足'})
+ dashou_profile.yajin -= amount
+ dashou_profile.save()
+ after = dashou_profile.yajin
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ yajin=after,
+ yyajin=before['yajin'],
+ )
+ return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after)}})
+
+ elif caozuo == 'jia_yajin':
+ if '001bb' not in permissions:
+ return Response({'code': 403, 'msg': '无权限加打手押金'})
+ amount = Decimal(str(value))
+ dashou_profile.yajin += amount
+ dashou_profile.save()
+ after = dashou_profile.yajin
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ yajin=after,
+ yyajin=before['yajin'],
+ )
+ return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after)}})
+
+ # ---------- 积分操作 ----------
+ elif caozuo == 'jian_jifen':
+ if '001cc' not in permissions:
+ return Response({'code': 403, 'msg': '无权限减打手积分'})
+ jifen = int(value)
+ if dashou_profile.jifen < jifen:
+ return Response({'code': 400, 'msg': '积分不足'})
+ dashou_profile.jifen -= jifen
+ dashou_profile.save()
+ after = dashou_profile.jifen
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ jifen=after,
+ yjifen=before['jifen'],
+ )
+ return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after}})
+
+ elif caozuo == 'jia_jifen':
+ if '001dd' not in permissions:
+ return Response({'code': 403, 'msg': '无权限加打手积分'})
+ jifen = int(value)
+ dashou_profile.jifen += jifen
+ dashou_profile.save()
+ after = dashou_profile.jifen
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ jifen=after,
+ yjifen=before['jifen'],
+ )
+ return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after}})
+
+ # ---------- 状态修改 ----------
+ elif caozuo == 'gai_zhanghaozhuangtai':
+ if '001ee' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改打手账号状态'})
+ new_status = int(value)
+ if new_status not in [0, 1]:
+ return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'})
+ dashou_profile.zhanghaozhuangtai = new_status
+ dashou_profile.save()
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ qitashuoming=f'修改账号状态为{new_status}',
+ )
+ return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}})
+
+ elif caozuo == 'gai_zaixianzhuangtai':
+ if '001ee' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改打手在线状态'})
+ new_status = int(value)
+ if new_status not in [0, 1]:
+ return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'})
+ dashou_profile.zaixianzhuangtai = new_status
+ dashou_profile.save()
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ qitashuoming=f'修改在线状态为{new_status}',
+ )
+ return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}})
+
+ elif caozuo == 'gai_zhuangtai':
+ if '001ee' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改打手接单状态'})
+ new_status = int(value)
+ if new_status not in [0, 1]:
+ return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'})
+ dashou_profile.zhuangtai = new_status
+ dashou_profile.save()
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ qitashuoming=f'修改接单状态为{new_status}',
+ )
+ return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}})
+
+ # ---------- 会员操作 ----------
+ elif caozuo == 'jia_huiyuan':
+ if '001ff' not in permissions:
+ return Response({'code': 403, 'msg': '无权限给打手加会员'})
+ if not huiyuan_id:
+ return Response({'code': 400, 'msg': '缺少会员ID'})
+ days = int(value) if value else 30
+ try:
+ huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id)
+ except Huiyuan.DoesNotExist:
+ return Response({'code': 404, 'msg': '会员类型不存在'})
+ record, created = Huiyuangoumai.objects.select_for_update().get_or_create(
+ yonghu_id=dashou_id,
+ huiyuan_id=huiyuan_id,
+ defaults={
+ 'huiyuan_zhuangtai': 1,
+ 'jieshao': huiyuan.jieshao,
+ 'daoqi_time': timezone.now() + timedelta(days=days),
+ }
+ )
+ if not created:
+ new_daoqi = record.daoqi_time + timedelta(days=days)
+ record.daoqi_time = new_daoqi
+ record.huiyuan_zhuangtai = 1
+ record.save()
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ huiyuants=days,
+ huiyuan_id=huiyuan_id,
+ qitashuoming=f'加会员 {huiyuan_id},增加{days}天',
+ )
+ return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
+
+ elif caozuo == 'jian_huiyuan':
+ if '001gg' not in permissions:
+ return Response({'code': 403, 'msg': '无权限给打手减会员'})
+ if not huiyuan_id:
+ return Response({'code': 400, 'msg': '缺少会员ID'})
+ try:
+ record = Huiyuangoumai.objects.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id)
+ except Huiyuangoumai.DoesNotExist:
+ return Response({'code': 404, 'msg': '该打手未购买此会员'})
+ record.delete()
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ huiyuan_id=huiyuan_id,
+ qitashuoming=f'移除会员 {huiyuan_id}',
+ )
+ return Response({'code': 0, 'msg': '减会员成功'})
+
+ # ---------- 称号修改(金牌打手) ----------
+ elif caozuo == 'gai_chenghao':
+ if '001hh' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改打手称号'})
+ new_chenghao = str(value).strip()
+ if not new_chenghao:
+ return Response({'code': 400, 'msg': '称号不能为空'})
+ # 可限制合法值,这里只记录日志
+ dashou_profile.chenghao = new_chenghao
+ dashou_profile.save()
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ qitashuoming=f'修改称号:{before["chenghao"] or "无"} → {new_chenghao}',
+ )
+ return Response({'code': 0, 'msg': '修改称号成功', 'data': {'new_chenghao': new_chenghao}})
+
+ else:
+ return Response({'code': 400, 'msg': '未知操作类型'})
+
+ except Exception as e:
+ logger.error(f"修改打手信息异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+
+
+
+class KefuGetShangjiaListView(APIView):
+ """
+ 客服/管理员获取商家列表接口
+ 需要拥有以下任一权限:1100a, 1100b
+ 请求:POST /houtai/hqsjgllb
+ 参数:
+ username: 客服手机号(用于权限校验,必填)
+ keyword: 搜索关键词(可选,搜索商家用户ID或昵称)
+ balance_op: 余额比较符,gte(>=) / lte(<=)(可选)
+ balance_value: 余额数值(可选,需与balance_op同时存在)
+ order_count_op: 派单量比较符,gte(>=) / lte(<=)(可选)
+ order_count_value: 派单量数值(可选,需与order_count_op同时存在)
+ status: 商家状态,1=正常,2=禁用(可选)
+ page: 页码,从1开始,默认1
+ page_size: 每页数量,默认20
+ 返回:
+ code: 0 成功,其他失败
+ data: {
+ list: [商家对象],
+ total: int, // 总商家数
+ }
+ """
+ permission_classes = [] # 不依赖全局认证,手动校验
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ username = request.data.get('username', '').strip()
+ keyword = request.data.get('keyword', '').strip()
+ balance_op = request.data.get('balance_op', '').strip().lower()
+ balance_value_str = request.data.get('balance_value')
+ order_count_op = request.data.get('order_count_op', '').strip().lower()
+ order_count_value_str = request.data.get('order_count_value')
+ status_str = request.data.get('status')
+
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 20))
+ if page < 1 or page_size < 1:
+ raise ValueError
+ except ValueError:
+ return Response({'code': 400, 'msg': '分页参数错误'})
+
+ if not username:
+ return Response({'code': 401, 'msg': '认证失败,缺少username'})
+
+ # 2. 公共权限校验(复用现有函数 verify_kefu_permission)
+ # 假设 verify_kefu_permission 定义在某个公共模块,这里引入
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 检查商家管理权限(1100a 或 1100b)
+ required_perms = {'1100a', '1100b'}
+ if not any(p in permissions for p in required_perms):
+ return Response({'code': 403, 'msg': '您没有权限查看商家列表'})
+
+ # 4. 构建商家查询集(商家扩展表 + 关联用户主表)
+ shangjia_qs = UserShangjia.objects.select_related('user').all()
+
+ # 关键词筛选(用户ID或昵称)
+ if keyword:
+ shangjia_qs = shangjia_qs.filter(
+ Q(user__yonghuid__icontains=keyword) |
+ Q(nicheng__icontains=keyword)
+ )
+
+ # 余额筛选
+ if balance_op and balance_value_str is not None and balance_value_str != '':
+ try:
+ balance_value = Decimal(str(balance_value_str))
+ if balance_op == 'gte':
+ shangjia_qs = shangjia_qs.filter(yue__gte=balance_value)
+ elif balance_op == 'lte':
+ shangjia_qs = shangjia_qs.filter(yue__lte=balance_value)
+ else:
+ return Response({'code': 400, 'msg': '余额比较符只支持gte或lte'})
+ except Exception:
+ return Response({'code': 400, 'msg': '余额数值格式错误'})
+
+ # 派单总量筛选(fabu字段)
+ if order_count_op and order_count_value_str is not None and order_count_value_str != '':
+ try:
+ order_count_value = int(order_count_value_str)
+ if order_count_op == 'gte':
+ shangjia_qs = shangjia_qs.filter(fabu__gte=order_count_value)
+ elif order_count_op == 'lte':
+ shangjia_qs = shangjia_qs.filter(fabu__lte=order_count_value)
+ else:
+ return Response({'code': 400, 'msg': '派单量比较符只支持gte或lte'})
+ except Exception:
+ return Response({'code': 400, 'msg': '派单量数值必须为整数'})
+
+ # 商家状态筛选
+ if status_str is not None and status_str != '':
+ try:
+ status_val = int(status_str)
+ if status_val not in [1, 2]:
+ return Response({'code': 400, 'msg': '商家状态值无效,应为1或2'})
+ shangjia_qs = shangjia_qs.filter(zhuangtai=status_val)
+ except ValueError:
+ return Response({'code': 400, 'msg': '商家状态格式错误'})
+
+ # 5. 统计总商家数(应用筛选后)
+ total = shangjia_qs.count()
+
+ # 6. 分页
+ offset = (page - 1) * page_size
+ shangjia_list = shangjia_qs.order_by('-create_time')[offset:offset + page_size]
+
+ # 7. 构建返回数据
+ result = []
+ for shangjia in shangjia_list:
+ user = shangjia.user
+ result.append({
+ 'yonghuid': user.yonghuid,
+ 'avatar': user.avatar or '',
+ 'nicheng': shangjia.nicheng or '',
+ 'yue': str(shangjia.yue), # 转换为字符串,避免前端精度问题
+ 'fabu': shangjia.fabu,
+ 'chengjiao': shangjia.chengjiao,
+ 'zhuangtai': shangjia.zhuangtai, # 1=正常,2=禁用
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': result,
+ 'total': total,
+ }
+ })
+
+
+
+
+
+
+# yonghu/views.py 中添加
+
+class KefuGetShangjiaDetailView(APIView):
+ """
+ 客服/管理员获取商家详情接口
+ 需要权限:1100a 或 1100b
+ 请求:POST /houtai/hthqsjxq
+ 参数:
+ username: 客服手机号(用于权限校验,必填)
+ yonghuid: 商家用户ID(必填)
+ 返回:
+ code: 0 成功
+ data: 商家所有字段(含主表头像等)
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ yonghuid = request.data.get('yonghuid', '').strip()
+
+ if not username:
+ return Response({'code': 401, 'msg': '认证失败,缺少username'})
+ if not yonghuid:
+ return Response({'code': 400, 'msg': '缺少商家ID'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ required_perms = {'1100a', '1100b'}
+ if not any(p in permissions for p in required_perms):
+ return Response({'code': 403, 'msg': '您没有权限查看商家详情'})
+
+ # 查询商家
+ try:
+ shangjia = UserShangjia.objects.select_related('user').get(user__yonghuid=yonghuid)
+ except UserShangjia.DoesNotExist:
+ return Response({'code': 404, 'msg': '商家不存在'})
+
+ user = shangjia.user
+
+ data = {
+ 'yonghuid': user.yonghuid,
+ 'avatar': user.avatar or '',
+ 'nicheng': shangjia.nicheng,
+ 'zhuangtai': shangjia.zhuangtai, # 1正常,2禁用
+ 'dianhua': shangjia.dianhua,
+ 'wechat': shangjia.wechat,
+ 'yue': str(shangjia.yue),
+ 'fabu': shangjia.fabu,
+ 'chengjiao': shangjia.chengjiao,
+ 'tuikuan': shangjia.tuikuan,
+ 'jinridingdan': shangjia.jinridingdan,
+ 'jinriliushui': str(shangjia.jinriliushui),
+ 'jinyuedingdan': shangjia.jinyuedingdan,
+ 'jinyueliushui': str(shangjia.jinyueliushui),
+ 'create_time': shangjia.create_time.isoformat() if shangjia.create_time else None,
+ }
+
+ return Response({'code': 0, 'data': data})
+
+
+
+
+
+
+
+class KefuUpdateShangjiaView(APIView):
+ """
+ 客服/管理员修改商家信息(余额、状态)
+ POST /houtai/xgsjxx
+ 参数:
+ username, yonghuid, action
+ action可选:add_balance, sub_balance, ban, unban
+ amount(add_balance/sub_balance时必填)
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ yonghuid = request.data.get('yonghuid', '').strip()
+ action = request.data.get('action', '').strip().lower()
+
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+ if not yonghuid:
+ return Response({'code': 400, 'msg': '缺少商家ID'})
+ if action not in ('add_balance', 'sub_balance', 'ban', 'unban'):
+ return Response({'code': 400, 'msg': 'action参数无效'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ # 操作权限校验
+ if action in ('add_balance', 'ban', 'unban') and '1100a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限执行此操作(需要1100a)'})
+ if action == 'sub_balance' and '1100b' not in permissions:
+ return Response({'code': 403, 'msg': '无权限减少商家余额(需要1100b)'})
+
+ # 使用事务,select_for_update 必须在事务内
+ with transaction.atomic():
+ try:
+ shangjia = UserShangjia.objects.select_for_update().get(user__yonghuid=yonghuid)
+ except UserShangjia.DoesNotExist:
+ return Response({'code': 404, 'msg': '商家不存在'})
+
+ # 增加余额
+ if action == 'add_balance':
+ amount_str = request.data.get('amount')
+ try:
+ amount = Decimal(str(amount_str))
+ if amount <= 0 or amount > 1000000:
+ return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'})
+ except:
+ return Response({'code': 400, 'msg': '金额格式错误'})
+ new_yue = shangjia.yue + amount
+ if new_yue > 999999999.99:
+ return Response({'code': 400, 'msg': '增加后余额超出上限'})
+ shangjia.yue = new_yue
+ shangjia.save(update_fields=['yue'])
+ return Response({'code': 0, 'msg': '余额增加成功', 'data': {'new_balance': str(shangjia.yue)}})
+
+ # 减少余额
+ if action == 'sub_balance':
+ amount_str = request.data.get('amount')
+ try:
+ amount = Decimal(str(amount_str))
+ if amount <= 0 or amount > 1000000:
+ return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'})
+ except:
+ return Response({'code': 400, 'msg': '金额格式错误'})
+ if shangjia.yue < amount:
+ return Response({'code': 400, 'msg': '余额不足'})
+ shangjia.yue = shangjia.yue - amount
+ shangjia.save(update_fields=['yue'])
+ return Response({'code': 0, 'msg': '余额减少成功', 'data': {'new_balance': str(shangjia.yue)}})
+
+ # 封禁
+ if action == 'ban':
+ if shangjia.zhuangtai == 2:
+ return Response({'code': 400, 'msg': '商家已是封禁状态'})
+ shangjia.zhuangtai = 2
+ shangjia.save(update_fields=['zhuangtai'])
+ return Response({'code': 0, 'msg': '商家已封禁', 'data': {'zhuangtai': 2}})
+
+ # 解封
+ if action == 'unban':
+ if shangjia.zhuangtai == 1:
+ return Response({'code': 400, 'msg': '商家已是正常状态'})
+ shangjia.zhuangtai = 1
+ shangjia.save(update_fields=['zhuangtai'])
+ return Response({'code': 0, 'msg': '商家已解封', 'data': {'zhuangtai': 1}})
+
+
+
+
+
+
+
+class GetProductBaseDataView(APIView):
+ """
+ 获取商品基础数据(类型、专区、会员)
+ 权限:2200a
+ POST /houtai/htsphqjcsx
+ 参数:username
+ 返回:
+ code:0, data: {
+ type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...],
+ zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...],
+ member_list: [ {huiyuan_id, jieshao, jiage}, ...]
+ }
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '2200a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限管理商品'})
+
+ # 获取所有商品类型(按 paixu 升序)
+ type_qs = ShangpinLeixing.objects.all().order_by('paixu')
+ type_list = []
+ for t in type_qs:
+ # 🔥 修改:统计该类型下的公共商品数量(店铺为空且店铺商品类型为空)
+ product_count = Shangpin.objects.filter(
+ leixing_id=t.id,
+ dianpu__isnull=True, # 所属店铺为空
+ dianpu_leixing__isnull=True # 店铺商品类型为空
+ ).count()
+ type_list.append({
+ 'id': t.id,
+ 'jieshao': t.jieshao or '',
+ 'tupian_url': t.tupian_url or '',
+ 'paixu': t.paixu,
+ 'shenhezhuangtai': t.shenhezhuangtai,
+ 'product_count': product_count,
+ })
+
+ # 获取所有专区(按 paixu 升序)
+ zone_qs = ShangpinZhuanqu.objects.all().order_by('paixu')
+ zone_list = []
+ for z in zone_qs:
+ # 🔥 修改:统计该专区下的公共商品数量(店铺为空且店铺商品类型为空)
+ product_count = Shangpin.objects.filter(
+ zhuanqu_id=z.id,
+ dianpu__isnull=True, # 所属店铺为空
+ dianpu_leixing__isnull=True # 店铺商品类型为空
+ ).count()
+ zone_list.append({
+ 'id': z.id,
+ 'mingzi': z.mingzi or '',
+ 'leixing_id': z.leixing_id,
+ 'paixu': z.paixu,
+ 'shenhezhuangtai': z.shenhezhuangtai,
+ 'product_count': product_count,
+ })
+
+ # 获取所有会员(只需 ID、介绍、价格)
+ member_list = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage'))
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'type_list': type_list,
+ 'zone_list': zone_list,
+ 'member_list': member_list,
+ }
+ })
+
+
+
+class GetProductListView(APIView):
+ """
+ 获取商品列表(支持筛选、排序)
+ 权限:2200a
+ POST /houtai/hthqsplb
+ 参数:
+ username: 必填
+ leixing_id: 类型ID(可选)
+ zhuanqu_id: 专区ID(可选)
+ keyword: 搜索关键词(商品标题/介绍)
+ sales_op: 销量比较符 gte/lte
+ sales_value: 销量数值
+ stock_op: 库存比较符 gte/lte
+ stock_value: 库存数值
+ sort_field: 排序字段 zhenshi_xiaoliang / jiage
+ sort_order: asc/desc
+ page: 页码(默认1)
+ page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留)
+ 返回:
+ code:0, data: { list: [...], total }
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '2200a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限管理商品'})
+
+ # 获取参数
+ leixing_id = request.data.get('leixing_id')
+ zhuanqu_id = request.data.get('zhuanqu_id')
+ keyword = request.data.get('keyword', '').strip()
+ sales_op = request.data.get('sales_op', '').strip().lower()
+ sales_value = request.data.get('sales_value')
+ stock_op = request.data.get('stock_op', '').strip().lower()
+ stock_value = request.data.get('stock_value')
+ sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage
+ sort_order = request.data.get('sort_order', 'desc').lower()
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 1000))
+ if page < 1 or page_size < 1:
+ raise ValueError
+ except:
+ return Response({'code': 400, 'msg': '分页参数错误'})
+
+ # 🔥 修改:构建查询集,增加公共商品筛选(店铺为空且店铺商品类型为空)
+ qs = Shangpin.objects.filter(
+ dianpu__isnull=True, # 所属店铺为空
+ dianpu_leixing__isnull=True # 店铺商品类型为空
+ )
+
+ if leixing_id:
+ try:
+ leixing_id = int(leixing_id)
+ qs = qs.filter(leixing_id=leixing_id)
+ except:
+ pass
+ if zhuanqu_id:
+ try:
+ zhuanqu_id = int(zhuanqu_id)
+ qs = qs.filter(zhuanqu_id=zhuanqu_id)
+ except:
+ pass
+ if keyword:
+ qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword))
+
+ # 销量筛选
+ if sales_op and sales_value is not None:
+ try:
+ sales_val = int(sales_value)
+ if sales_op == 'gte':
+ qs = qs.filter(zhenshi_xiaoliang__gte=sales_val)
+ elif sales_op == 'lte':
+ qs = qs.filter(zhenshi_xiaoliang__lte=sales_val)
+ except:
+ pass
+
+ # 库存筛选
+ if stock_op and stock_value is not None:
+ try:
+ stock_val = int(stock_value)
+ if stock_op == 'gte':
+ qs = qs.filter(kucun__gte=stock_val)
+ elif stock_op == 'lte':
+ qs = qs.filter(kucun__lte=stock_val)
+ except:
+ pass
+
+ # 排序
+ if sort_field in ('zhenshi_xiaoliang', 'jiage'):
+ if sort_order == 'asc':
+ qs = qs.order_by(sort_field)
+ else:
+ qs = qs.order_by(f'-{sort_field}')
+ else:
+ qs = qs.order_by('-create_time') # 默认按创建时间倒序
+
+ # 分页
+ total = qs.count()
+ offset = (page - 1) * page_size
+ products = qs[offset:offset + page_size]
+
+ # 构建返回数据
+ product_list = []
+ for p in products:
+ product_list.append({
+ 'id': p.id,
+ 'biaoqian': p.biaoqian or '',
+ 'jiage': str(p.jiage),
+ 'kucun': p.kucun,
+ 'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
+ 'tupian_url': p.tupian_url or '',
+ 'jieshao': p.jieshao or '',
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': product_list,
+ 'total': total,
+ }
+ })
+
+
+
+
+'''class GetProductBaseDataView(APIView):
+ """
+ 获取商品基础数据(类型、专区、会员)
+ 权限:2200a
+ POST /houtai/htsphqjcsx
+ 参数:username
+ 返回:
+ code:0, data: {
+ type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...],
+ zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...],
+ member_list: [ {huiyuan_id, jieshao, jiage}, ...]
+ }
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '2200a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限管理商品'})
+
+ # 获取所有商品类型(按 paixu 升序)
+ type_qs = ShangpinLeixing.objects.all().order_by('paixu')
+ type_list = []
+ for t in type_qs:
+ # 统计该类型下的商品数量(忽略审核状态?通常统计所有商品,可根据需求调整)
+ product_count = Shangpin.objects.filter(leixing_id=t.id).count()
+ type_list.append({
+ 'id': t.id,
+ 'jieshao': t.jieshao or '',
+ 'tupian_url': t.tupian_url or '',
+ 'paixu': t.paixu,
+ 'shenhezhuangtai': t.shenhezhuangtai,
+ 'product_count': product_count,
+ })
+
+ # 获取所有专区(按 paixu 升序)
+ zone_qs = ShangpinZhuanqu.objects.all().order_by('paixu')
+ zone_list = []
+ for z in zone_qs:
+ product_count = Shangpin.objects.filter(zhuanqu_id=z.id).count()
+ zone_list.append({
+ 'id': z.id,
+ 'mingzi': z.mingzi or '',
+ 'leixing_id': z.leixing_id,
+ 'paixu': z.paixu,
+ 'shenhezhuangtai': z.shenhezhuangtai,
+ 'product_count': product_count,
+ })
+
+ # 获取所有会员(只需 ID、介绍、价格)
+ member_list = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage'))
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'type_list': type_list,
+ 'zone_list': zone_list,
+ 'member_list': member_list,
+ }
+ })
+
+
+class GetProductListView(APIView):
+ """
+ 获取商品列表(支持筛选、排序)
+ 权限:2200a
+ POST /houtai/hthqsplb
+ 参数:
+ username: 必填
+ leixing_id: 类型ID(可选)
+ zhuanqu_id: 专区ID(可选)
+ keyword: 搜索关键词(商品标题/介绍)
+ sales_op: 销量比较符 gte/lte
+ sales_value: 销量数值
+ stock_op: 库存比较符 gte/lte
+ stock_value: 库存数值
+ sort_field: 排序字段 zhenshi_xiaoliang / jiage
+ sort_order: asc/desc
+ page: 页码(默认1)
+ page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留)
+ 返回:
+ code:0, data: { list: [...], total }
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '2200a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限管理商品'})
+
+ # 获取参数
+ leixing_id = request.data.get('leixing_id')
+ zhuanqu_id = request.data.get('zhuanqu_id')
+ keyword = request.data.get('keyword', '').strip()
+ sales_op = request.data.get('sales_op', '').strip().lower()
+ sales_value = request.data.get('sales_value')
+ stock_op = request.data.get('stock_op', '').strip().lower()
+ stock_value = request.data.get('stock_value')
+ sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage
+ sort_order = request.data.get('sort_order', 'desc').lower()
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 1000))
+ if page < 1 or page_size < 1:
+ raise ValueError
+ except:
+ return Response({'code': 400, 'msg': '分页参数错误'})
+
+ # 构建查询集
+ qs = Shangpin.objects.all()
+
+ if leixing_id:
+ try:
+ leixing_id = int(leixing_id)
+ qs = qs.filter(leixing_id=leixing_id)
+ except:
+ pass
+ if zhuanqu_id:
+ try:
+ zhuanqu_id = int(zhuanqu_id)
+ qs = qs.filter(zhuanqu_id=zhuanqu_id)
+ except:
+ pass
+ if keyword:
+ qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword))
+
+ # 销量筛选
+ if sales_op and sales_value is not None:
+ try:
+ sales_val = int(sales_value)
+ if sales_op == 'gte':
+ qs = qs.filter(zhenshi_xiaoliang__gte=sales_val)
+ elif sales_op == 'lte':
+ qs = qs.filter(zhenshi_xiaoliang__lte=sales_val)
+ except:
+ pass
+
+ # 库存筛选
+ if stock_op and stock_value is not None:
+ try:
+ stock_val = int(stock_value)
+ if stock_op == 'gte':
+ qs = qs.filter(kucun__gte=stock_val)
+ elif stock_op == 'lte':
+ qs = qs.filter(kucun__lte=stock_val)
+ except:
+ pass
+
+ # 排序
+ if sort_field in ('zhenshi_xiaoliang', 'jiage'):
+ if sort_order == 'asc':
+ qs = qs.order_by(sort_field)
+ else:
+ qs = qs.order_by(f'-{sort_field}')
+ else:
+ qs = qs.order_by('-create_time') # 默认按创建时间倒序
+
+ # 分页
+ total = qs.count()
+ offset = (page - 1) * page_size
+ products = qs[offset:offset + page_size]
+
+ # 构建返回数据
+ product_list = []
+ for p in products:
+ product_list.append({
+ 'id': p.id,
+ 'biaoqian': p.biaoqian or '',
+ 'jiage': str(p.jiage),
+ 'kucun': p.kucun,
+ 'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
+ 'tupian_url': p.tupian_url or '',
+ 'jieshao': p.jieshao or '',
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': product_list,
+ 'total': total,
+ }
+ })'''
+
+
+class SaveProductOrderView(APIView):
+ """
+ 保存商品类型或专区的排序
+ 权限:2200a
+ POST /houtai/htsppaixu
+ 参数:
+ username: 必填
+ type: 'leixing' 或 'zhuanqu'
+ leixing_id: 当 type='zhuanqu' 时必填(用于限定该类型下的专区)
+ items: [ {id, paixu}, ... ] # 每个项目的新排序值(从1开始)
+ 返回:
+ code:0 成功
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '2200a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限管理商品'})
+
+ sort_type = request.data.get('type', '').strip().lower()
+ if sort_type not in ('leixing', 'zhuanqu'):
+ return Response({'code': 400, 'msg': 'type参数必须为leixing或zhuanqu'})
+
+ items = request.data.get('items')
+ if not isinstance(items, list) or len(items) == 0:
+ return Response({'code': 400, 'msg': 'items参数无效'})
+
+ # 校验每个 item 的格式
+ for item in items:
+ if not isinstance(item, dict) or 'id' not in item or 'paixu' not in item:
+ return Response({'code': 400, 'msg': 'items内每项需包含id和paixu'})
+ try:
+ item['id'] = int(item['id'])
+ item['paixu'] = int(item['paixu'])
+ if item['paixu'] < 1:
+ return Response({'code': 400, 'msg': 'paixu必须为正整数'})
+ except:
+ return Response({'code': 400, 'msg': 'id或paixu格式错误'})
+
+ with transaction.atomic():
+ if sort_type == 'leixing':
+ # 更新商品类型的排序
+ for item in items:
+ ShangpinLeixing.objects.filter(id=item['id']).update(paixu=item['paixu'])
+ else: # zhuanqu
+ leixing_id = request.data.get('leixing_id')
+ if not leixing_id:
+ return Response({'code': 400, 'msg': '更新专区排序时必须提供leixing_id'})
+ try:
+ leixing_id = int(leixing_id)
+ except:
+ return Response({'code': 400, 'msg': 'leixing_id格式错误'})
+ # 可选:检查该专区是否属于该类型,但不强制,由前端保证
+ for item in items:
+ ShangpinZhuanqu.objects.filter(id=item['id']).update(paixu=item['paixu'])
+
+ return Response({'code': 0, 'msg': '排序保存成功'})
+
+
+class AddProductView(APIView):
+ """
+ 添加商品(含图片上传)
+ 权限:2200a
+ POST /houtai/htfbsp
+ 参数:multipart/form-data,包含所有商品字段及图片文件
+ 字段说明见代码
+ """
+ permission_classes = []
+ parser_classes = [MultiPartParser, FormParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '2200a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限管理商品'})
+
+ # 获取表单字段
+ biaoqian = request.data.get('biaoqian', '').strip()
+ jiage_str = request.data.get('jiage')
+ kucun_str = request.data.get('kucun')
+ duiwai_xiaoliang_str = request.data.get('duiwai_xiaoliang', '0')
+ leixing_id_str = request.data.get('leixing_id')
+ zhuanqu_id_str = request.data.get('zhuanqu_id')
+ yaoqiuleixing_str = request.data.get('yaoqiuleixing', '1')
+ huiyuan_id = request.data.get('huiyuan_id', '').strip()
+ yongjin_str = request.data.get('yongjin', '0')
+ fencheng_type = request.data.get('fencheng_type', 'default')
+ ewai_dashou_fencheng_str = request.data.get('ewai_dashou_fencheng', '0')
+ jieshao = request.data.get('jieshao', '').strip()
+ xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip()
+ # 图片文件
+ image_file = request.FILES.get('file') # 前端上传时字段名为 'file'
+
+ # 必填字段校验
+ if not biaoqian:
+ return Response({'code': 400, 'msg': '商品标题不能为空'})
+ try:
+ jiage = Decimal(jiage_str)
+ if jiage <= 0:
+ return Response({'code': 400, 'msg': '价格必须大于0'})
+ except:
+ return Response({'code': 400, 'msg': '价格格式错误'})
+ try:
+ kucun = int(kucun_str)
+ if kucun < 0:
+ return Response({'code': 400, 'msg': '库存不能为负数'})
+ except:
+ return Response({'code': 400, 'msg': '库存格式错误'})
+ try:
+ duiwai_xiaoliang = int(duiwai_xiaoliang_str)
+ except:
+ duiwai_xiaoliang = 0
+ try:
+ leixing_id = int(leixing_id_str)
+ except:
+ return Response({'code': 400, 'msg': '商品类型ID无效'})
+ try:
+ zhuanqu_id = int(zhuanqu_id_str)
+ except:
+ return Response({'code': 400, 'msg': '商品专区ID无效'})
+ try:
+ yaoqiuleixing = int(yaoqiuleixing_str)
+ if yaoqiuleixing not in (1, 2):
+ raise ValueError
+ except:
+ return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'})
+
+ # 校验类型和专区的存在性及关联性
+ try:
+ leixing_obj = ShangpinLeixing.objects.get(id=leixing_id)
+ except ShangpinLeixing.DoesNotExist:
+ return Response({'code': 400, 'msg': '商品类型不存在'})
+ try:
+ zhuanqu_obj = ShangpinZhuanqu.objects.get(id=zhuanqu_id)
+ if zhuanqu_obj.leixing_id != leixing_id:
+ return Response({'code': 400, 'msg': '专区不属于该商品类型'})
+ except ShangpinZhuanqu.DoesNotExist:
+ return Response({'code': 400, 'msg': '商品专区不存在'})
+
+ # 处理抢单要求相关字段
+ huiyuan_id_val = None
+ yongjin_val = None
+ if yaoqiuleixing == 1:
+ # 会员要求
+ if not huiyuan_id:
+ # 未传递时,尝试从商品类型中获取默认会员ID
+ huiyuan_id_val = leixing_obj.huiyuan_id
+ if not huiyuan_id_val:
+ return Response({'code': 400, 'msg': '未指定会员且商品类型无默认会员'})
+ else:
+ huiyuan_id_val = huiyuan_id
+ # 验证会员是否存在
+ if not Huiyuan.objects.filter(huiyuan_id=huiyuan_id_val).exists():
+ return Response({'code': 400, 'msg': '指定的会员不存在'})
+ else:
+ # 佣金要求
+ try:
+ yongjin_val = Decimal(yongjin_str)
+ if yongjin_val < 0:
+ return Response({'code': 400, 'msg': '佣金不能为负数'})
+ except:
+ # 若未传递或无效,尝试从商品类型中获取默认佣金
+ if leixing_obj.yongjin is not None:
+ yongjin_val = leixing_obj.yongjin
+ else:
+ return Response({'code': 400, 'msg': '佣金格式错误且类型无默认佣金'})
+
+ # 处理打手分成方式
+ kaioi_ewai = False
+ ewai_fencheng = Decimal('0')
+ if fencheng_type == 'custom':
+ try:
+ ewai_fencheng = Decimal(ewai_dashou_fencheng_str)
+ if ewai_fencheng < 0:
+ return Response({'code': 400, 'msg': '自定义分成金额不能为负数'})
+ kaioi_ewai = True
+ except:
+ return Response({'code': 400, 'msg': '自定义分成金额格式错误'})
+
+ # 图片上传处理
+ tupian_url = ''
+ if image_file:
+ # 验证图片
+ valid, msg = validate_image(image_file)
+ if not valid:
+ return Response({'code': 400, 'msg': f'图片无效:{msg}'})
+ # 生成唯一文件名
+ ext = image_file.name.split('.')[-1].lower()
+ if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']:
+ return Response({'code': 400, 'msg': '不支持的图片格式'})
+ timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
+ random_str = uuid.uuid4().hex[:8]
+ filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}"
+ # 上传到 OSS
+ uploaded_url = upload_to_oss(image_file, filename)
+ if not uploaded_url:
+ return Response({'code': 500, 'msg': '图片上传失败,请稍后重试'})
+ tupian_url = filename # 存储相对路径,前端拼接域名
+ else:
+ return Response({'code': 400, 'msg': '商品图片不能为空'})
+
+ # 创建商品
+ try:
+ with transaction.atomic():
+ product = Shangpin.objects.create(
+ biaoqian=biaoqian,
+ jiage=jiage,
+ kucun=kucun,
+ leixing_id=leixing_id,
+ zhuanqu_id=zhuanqu_id,
+ zhenshi_xiaoliang=0, # 初始真实销量为0
+ duiwai_xiaoliang=duiwai_xiaoliang,
+ jieshao=jieshao,
+ xiadan_xuzhi=xiadan_xuzhi,
+ tupian_url=tupian_url,
+ yaoqiuleixing=yaoqiuleixing,
+ huiyuan_id=huiyuan_id_val,
+ yongjin=yongjin_val,
+ kaioi_ewai_dashou_fencheng=kaioi_ewai,
+ ewai_dashou_fencheng=ewai_fencheng,
+ paixu=0, # 新商品默认排序最后
+ shenhezhuangtai=1, # 正常状态
+ )
+ except Exception as e:
+ logger.error(f"创建商品失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '创建商品失败,请稍后重试'})
+
+ return Response({
+ 'code': 0,
+ 'msg': '商品发布成功',
+ 'data': {
+ 'id': product.id,
+ 'tupian_url': tupian_url,
+ }
+ })
+
+
+# houtai/views_product.py 中添加
+
+class DeleteProductView(APIView):
+ """
+ 删除商品(物理删除,同时删除OSS图片)
+ 权限:2200a
+ POST /houtai/htscsp
+ 参数:
+ username: 客服/管理员账号
+ id: 商品ID
+ 返回:
+ code:0 成功
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ product_id = request.data.get('id')
+
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+ if not product_id:
+ return Response({'code': 400, 'msg': '缺少商品ID'})
+ try:
+ product_id = int(product_id)
+ except:
+ return Response({'code': 400, 'msg': '商品ID格式错误'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '2200a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限管理商品'})
+
+ # 查询商品是否存在
+ try:
+ product = Shangpin.objects.get(id=product_id)
+ except Shangpin.DoesNotExist:
+ return Response({'code': 404, 'msg': '商品不存在'})
+
+ # 先删除OSS上的图片
+ tupian_url = product.tupian_url
+ if tupian_url:
+ # 调用已有的 delete_from_oss 函数
+ delete_success = delete_from_oss(tupian_url)
+ if not delete_success:
+ logger.warning(f"删除商品图片失败: {tupian_url}, 商品ID: {product_id}")
+ # 注意:即使图片删除失败,也应继续删除数据库记录,避免数据不一致
+ # 但可以记录日志供后续人工处理
+
+ # 删除数据库记录
+ product.delete()
+
+ logger.info(f"商品删除成功: id={product_id}, 图片路径={tupian_url}, 操作人={username}")
+ return Response({'code': 0, 'msg': '商品删除成功'})
+
+
+
+
+
+
+
+class GetProductDetailView(APIView):
+ """
+ 获取商品详情(包含商品类型、专区、会员列表)
+ 权限:2200a
+ POST /houtai/htsphqxq
+ 参数:
+ username: 客服/管理员账号
+ id: 商品ID
+ 返回:
+ code:0, data: { product, type_list, zone_list, member_list }
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ product_id = request.data.get('id')
+
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+ if not product_id:
+ return Response({'code': 400, 'msg': '缺少商品ID'})
+ try:
+ product_id = int(product_id)
+ except ValueError:
+ return Response({'code': 400, 'msg': '商品ID格式错误'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '2200a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限管理商品'})
+
+ # 查询商品
+ try:
+ product = Shangpin.objects.get(id=product_id)
+ except Shangpin.DoesNotExist:
+ return Response({'code': 404, 'msg': '商品不存在'})
+
+ # 查询所有商品类型、专区、会员(用于前端下拉选择)
+ type_list = list(ShangpinLeixing.objects.all().values(
+ 'id', 'jieshao', 'tupian_url', 'shenhezhuangtai'
+ ))
+ zone_list = list(ShangpinZhuanqu.objects.all().values(
+ 'id', 'mingzi', 'leixing_id', 'shenhezhuangtai'
+ ))
+ member_list = list(Huiyuan.objects.all().values(
+ 'huiyuan_id', 'jieshao', 'jiage'
+ ))
+
+ # 构建商品返回数据
+ product_data = {
+ 'id': product.id,
+ 'biaoqian': product.biaoqian,
+ 'jiage': str(product.jiage),
+ 'kucun': product.kucun,
+ 'leixing_id': product.leixing_id,
+ 'zhuanqu_id': product.zhuanqu_id,
+ 'zhenshi_xiaoliang': product.zhenshi_xiaoliang,
+ 'duiwai_xiaoliang': product.duiwai_xiaoliang,
+ 'jieshao': product.jieshao,
+ 'xiadan_xuzhi': product.xiadan_xuzhi,
+ 'guize_tupian': product.guize_tupian,
+ 'yaoqiuleixing': product.yaoqiuleixing,
+ 'huiyuan_id': product.huiyuan_id,
+ 'yongjin': str(product.yongjin) if product.yongjin is not None else None,
+ 'tupian_url': product.tupian_url,
+ 'kaioi_ewai_dashou_fencheng': product.kaioi_ewai_dashou_fencheng,
+ 'ewai_dashou_fencheng': str(product.ewai_dashou_fencheng) if product.ewai_dashou_fencheng is not None else '0',
+ 'create_time': product.create_time.isoformat() if product.create_time else None,
+ 'shenhezhuangtai': product.shenhezhuangtai,
+ }
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'product': product_data,
+ 'type_list': type_list,
+ 'zone_list': zone_list,
+ 'member_list': member_list,
+ }
+ })
+
+
+
+class UpdateProductView(APIView):
+ """
+ 修改商品(支持商品图片和规则图片的更新)
+ 权限:2200a
+ POST /houtai/bcspxg
+ """
+ permission_classes = []
+ parser_classes = [MultiPartParser, FormParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ product_id = request.data.get('id')
+
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+ if not product_id:
+ return Response({'code': 400, 'msg': '缺少商品ID'})
+ try:
+ product_id = int(product_id)
+ except ValueError:
+ return Response({'code': 400, 'msg': '商品ID格式错误'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '2200a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限管理商品'})
+
+ # 使用事务,将 select_for_update 放在事务内
+ with transaction.atomic():
+ try:
+ product = Shangpin.objects.select_for_update().get(id=product_id)
+ except Shangpin.DoesNotExist:
+ return Response({'code': 404, 'msg': '商品不存在'})
+
+ # ---------- 1. 基础字段 ----------
+ biaoqian = request.data.get('biaoqian', '').strip()
+ if not biaoqian:
+ return Response({'code': 400, 'msg': '商品标题不能为空'})
+
+ try:
+ jiage = Decimal(str(request.data.get('jiage', 0)))
+ if jiage <= 0:
+ return Response({'code': 400, 'msg': '价格必须大于0'})
+ except:
+ return Response({'code': 400, 'msg': '价格格式错误'})
+
+ try:
+ kucun = int(request.data.get('kucun', 0))
+ if kucun < 0:
+ return Response({'code': 400, 'msg': '库存不能为负数'})
+ except:
+ return Response({'code': 400, 'msg': '库存格式错误'})
+
+ try:
+ duiwai_xiaoliang = int(request.data.get('duiwai_xiaoliang', 0))
+ except:
+ duiwai_xiaoliang = 0
+
+ try:
+ leixing_id = int(request.data.get('leixing_id'))
+ except:
+ return Response({'code': 400, 'msg': '商品类型ID无效'})
+
+ try:
+ zhuanqu_id = int(request.data.get('zhuanqu_id'))
+ except:
+ return Response({'code': 400, 'msg': '商品专区ID无效'})
+
+ try:
+ yaoqiuleixing = int(request.data.get('yaoqiuleixing', 1))
+ if yaoqiuleixing not in (1, 2):
+ raise ValueError
+ except:
+ return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'})
+
+ try:
+ shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1))
+ except:
+ shenhezhuangtai = 1
+
+ # 校验类型和专区存在性
+ if not ShangpinLeixing.objects.filter(id=leixing_id).exists():
+ return Response({'code': 400, 'msg': '商品类型不存在'})
+ if not ShangpinZhuanqu.objects.filter(id=zhuanqu_id, leixing_id=leixing_id).exists():
+ return Response({'code': 400, 'msg': '专区不存在或不属于该类型'})
+
+ # ---------- 2. 会员/佣金 ----------
+ huiyuan_id = None
+ yongjin = None
+ if yaoqiuleixing == 1:
+ huiyuan_id = request.data.get('huiyuan_id', '').strip() or None
+ if huiyuan_id and not Huiyuan.objects.filter(huiyuan_id=huiyuan_id).exists():
+ return Response({'code': 400, 'msg': '指定的会员不存在'})
+ else:
+ try:
+ yongjin = Decimal(str(request.data.get('yongjin', 0)))
+ if yongjin < 0:
+ return Response({'code': 400, 'msg': '佣金不能为负数'})
+ except:
+ yongjin = Decimal('0')
+
+ # ---------- 3. 打手分成 ----------
+ kaioi_ewai = request.data.get('kaioi_ewai_dashou_fencheng') == 'true'
+ ewai_fencheng = Decimal('0')
+ if kaioi_ewai:
+ try:
+ ewai_fencheng = Decimal(str(request.data.get('ewai_dashou_fencheng', 0)))
+ if ewai_fencheng < 0:
+ return Response({'code': 400, 'msg': '分成金额不能为负数'})
+ except:
+ return Response({'code': 400, 'msg': '分成金额格式错误'})
+
+ jieshao = request.data.get('jieshao', '').strip()
+ xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip()
+
+ # ---------- 4. 商品图片更新 ----------
+ tupian_url = product.tupian_url
+ tupian_file = request.FILES.get('tupian_file')
+ if tupian_file:
+ valid, msg = validate_image(tupian_file)
+ if not valid:
+ return Response({'code': 400, 'msg': f'商品图片无效:{msg}'})
+ ext = tupian_file.name.split('.')[-1].lower()
+ if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']:
+ return Response({'code': 400, 'msg': '不支持的图片格式'})
+ timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
+ random_str = uuid.uuid4().hex[:8]
+ new_filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}"
+ uploaded_url = upload_to_oss(tupian_file, new_filename)
+ if not uploaded_url:
+ return Response({'code': 500, 'msg': '商品图片上传失败'})
+ if product.tupian_url:
+ delete_from_oss(product.tupian_url)
+ tupian_url = new_filename
+
+ # ---------- 5. 规则图片更新 ----------
+ guize_tupian = product.guize_tupian
+ guize_file = request.FILES.get('guize_file')
+ if guize_file:
+ valid, msg = validate_image(guize_file)
+ if not valid:
+ return Response({'code': 400, 'msg': f'规则图片无效:{msg}'})
+ ext = guize_file.name.split('.')[-1].lower()
+ if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']:
+ return Response({'code': 400, 'msg': '不支持的图片格式'})
+ timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
+ random_str = uuid.uuid4().hex[:8]
+ new_filename = f"shangpin/guizetupian/{timestamp}_{random_str}.{ext}"
+ uploaded_url = upload_to_oss(guize_file, new_filename)
+ if not uploaded_url:
+ return Response({'code': 500, 'msg': '规则图片上传失败'})
+ if product.guize_tupian:
+ delete_from_oss(product.guize_tupian)
+ guize_tupian = new_filename
+
+ # ---------- 6. 更新数据库 ----------
+ product.biaoqian = biaoqian
+ product.jiage = jiage
+ product.kucun = kucun
+ product.duiwai_xiaoliang = duiwai_xiaoliang
+ product.leixing_id = leixing_id
+ product.zhuanqu_id = zhuanqu_id
+ product.yaoqiuleixing = yaoqiuleixing
+ product.huiyuan_id = huiyuan_id
+ product.yongjin = yongjin
+ product.kaioi_ewai_dashou_fencheng = kaioi_ewai
+ product.ewai_dashou_fencheng = ewai_fencheng
+ product.jieshao = jieshao
+ product.xiadan_xuzhi = xiadan_xuzhi
+ product.tupian_url = tupian_url
+ product.guize_tupian = guize_tupian
+ product.shenhezhuangtai = shenhezhuangtai
+ product.save()
+
+ return Response({'code': 0, 'msg': '商品修改成功', 'data': {'id': product.id}})
+
+
+
+
+
+class GetMemberListView(APIView):
+ """
+ 获取会员列表
+ 权限:3300a
+ POST /houtai/hthqhylb
+ 参数:username
+ 返回:code:0, data: { list: [...] }
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '3300a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限查看会员列表'})
+
+ members = Huiyuan.objects.all().order_by('-create_time')
+ member_list = []
+ for m in members:
+ member_list.append({
+ 'huiyuan_id': m.huiyuan_id,
+ 'jieshao': m.jieshao,
+ 'jtjieshao': m.jtjieshao,
+ 'jiage': str(m.jiage),
+ 'guanshifc': str(m.guanshifc),
+ 'zuzhangfc': str(m.zuzhangfc),
+ 'goumai_cishu': m.goumai_cishu,
+ 'create_time': m.create_time.isoformat() if m.create_time else None,
+ 'update_time': m.update_time.isoformat() if m.update_time else None,
+ })
+ return Response({'code': 0, 'data': {'list': member_list}})
+
+
+class UpdateMemberView(APIView):
+ """
+ 修改会员信息
+ 权限:3300a
+ POST /houtai/htxghyxx
+ 参数:
+ username, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
+ 返回:code:0 成功
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ huiyuan_id = request.data.get('huiyuan_id', '').strip()
+ jieshao = request.data.get('jieshao', '').strip()
+ jiage = request.data.get('jiage')
+ guanshifc = request.data.get('guanshifc')
+ zuzhangfc = request.data.get('zuzhangfc')
+ jtjieshao = request.data.get('jtjieshao', '').strip()
+
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+ if not huiyuan_id:
+ return Response({'code': 400, 'msg': '缺少会员ID'})
+ if not jieshao:
+ return Response({'code': 400, 'msg': '会员名称不能为空'})
+
+ try:
+ jiage = Decimal(str(jiage))
+ guanshifc = Decimal(str(guanshifc))
+ zuzhangfc = Decimal(str(zuzhangfc))
+ if jiage < 0 or guanshifc < 0 or zuzhangfc < 0:
+ raise ValueError
+ except:
+ return Response({'code': 400, 'msg': '金额格式错误'})
+
+ if guanshifc + zuzhangfc > jiage:
+ return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '3300a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改会员'})
+
+ with transaction.atomic():
+ try:
+ member = Huiyuan.objects.select_for_update().get(huiyuan_id=huiyuan_id)
+ except Huiyuan.DoesNotExist:
+ return Response({'code': 404, 'msg': '会员不存在'})
+
+ member.jieshao = jieshao
+ member.jiage = jiage
+ member.guanshifc = guanshifc
+ member.zuzhangfc = zuzhangfc
+ member.jtjieshao = jtjieshao
+ member.save()
+
+ return Response({'code': 0, 'msg': '修改成功'})
+
+
+
+class AddMemberView(APIView):
+ """
+ 添加会员
+ 权限:3300a
+ POST /houtai/httjhy
+ 参数:
+ username, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
+ 返回:code:0, data: { huiyuan_id }
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ jieshao = request.data.get('jieshao', '').strip()
+ jiage = request.data.get('jiage')
+ guanshifc = request.data.get('guanshifc')
+ zuzhangfc = request.data.get('zuzhangfc')
+ jtjieshao = request.data.get('jtjieshao', '').strip()
+
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+ if not jieshao:
+ return Response({'code': 400, 'msg': '会员名称不能为空'})
+
+ try:
+ jiage = Decimal(str(jiage))
+ guanshifc = Decimal(str(guanshifc))
+ zuzhangfc = Decimal(str(zuzhangfc))
+ if jiage <= 0 or guanshifc < 0 or zuzhangfc < 0:
+ raise ValueError
+ except:
+ return Response({'code': 400, 'msg': '金额格式错误'})
+
+ if guanshifc + zuzhangfc > jiage:
+ return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '3300a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限添加会员'})
+
+ # 生成唯一6位会员ID(数字+字母)
+ def generate_huiyuan_id():
+ while True:
+ new_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
+ if not Huiyuan.objects.filter(huiyuan_id=new_id).exists():
+ return new_id
+
+ with transaction.atomic():
+ new_id = generate_huiyuan_id()
+ member = Huiyuan.objects.create(
+ huiyuan_id=new_id,
+ jieshao=jieshao,
+ jiage=jiage,
+ guanshifc=guanshifc,
+ zuzhangfc=zuzhangfc,
+ jtjieshao=jtjieshao,
+ goumai_cishu=0
+ )
+
+ return Response({'code': 0, 'msg': '添加成功', 'data': {'huiyuan_id': member.huiyuan_id}})
+
+
+
+
+
+class GetGuanliListView(APIView):
+ """
+ 获取管事列表
+ 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一
+ POST /houtai/hthqgslb
+ 参数:
+ username, keyword(可选), balance_op/balance_value, commission_op/commission_value,
+ invite_count_op/invite_count_value, status, page, page_size
+ 返回:
+ code:0, data: { list: [...], total }
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+
+ # 权限校验(需要任一权限)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
+ if not any(p in permissions for p in required_perms):
+ return Response({'code': 403, 'msg': '您没有权限查看管事列表'})
+
+ # 分页参数
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 20))
+ if page < 1 or page_size < 1:
+ raise ValueError
+ except:
+ return Response({'code': 400, 'msg': '分页参数错误'})
+
+ # 筛选参数
+ keyword = request.data.get('keyword', '').strip()
+ balance_op = request.data.get('balance_op', '').strip().lower()
+ balance_value = request.data.get('balance_value')
+ commission_op = request.data.get('commission_op', '').strip().lower()
+ commission_value = request.data.get('commission_value')
+ invite_count_op = request.data.get('invite_count_op', '').strip().lower()
+ invite_count_value = request.data.get('invite_count_value')
+ status = request.data.get('status')
+
+ # 构建查询:从 UserMain 开始,关联 UserGuanshi 和 UserBoss(左连接获取昵称)
+ qs = UserMain.objects.filter(
+ guanshi_profile__isnull=False # 确保是管事
+ ).select_related('guanshi_profile').select_related('boss_profile')
+
+ # 关键词搜索(用户ID 或 昵称来自 boss_profile.nickname)
+ if keyword:
+ qs = qs.filter(
+ Q(yonghuid__icontains=keyword) |
+ Q(boss_profile__nickname__icontains=keyword)
+ )
+
+ # 余额筛选(使用 guanshi_profile.yue)
+ if balance_op and balance_value is not None and balance_value != '':
+ try:
+ balance_val = Decimal(str(balance_value))
+ if balance_op == 'gte':
+ qs = qs.filter(guanshi_profile__yue__gte=balance_val)
+ elif balance_op == 'lte':
+ qs = qs.filter(guanshi_profile__yue__lte=balance_val)
+ else:
+ return Response({'code': 400, 'msg': '余额比较符无效'})
+ except:
+ return Response({'code': 400, 'msg': '余额数值格式错误'})
+
+ # 分佣总额筛选(guanshi_profile.chongzhifenrun)
+ if commission_op and commission_value is not None and commission_value != '':
+ try:
+ commission_val = Decimal(str(commission_value))
+ if commission_op == 'gte':
+ qs = qs.filter(guanshi_profile__chongzhifenrun__gte=commission_val)
+ elif commission_op == 'lte':
+ qs = qs.filter(guanshi_profile__chongzhifenrun__lte=commission_val)
+ else:
+ return Response({'code': 400, 'msg': '分佣比较符无效'})
+ except:
+ return Response({'code': 400, 'msg': '分佣数值格式错误'})
+
+ # 邀请打手总数筛选(guanshi_profile.yaogingshuliang)
+ if invite_count_op and invite_count_value is not None and invite_count_value != '':
+ try:
+ invite_val = int(invite_count_value)
+ if invite_count_op == 'gte':
+ qs = qs.filter(guanshi_profile__yaogingshuliang__gte=invite_val)
+ elif invite_count_op == 'lte':
+ qs = qs.filter(guanshi_profile__yaogingshuliang__lte=invite_val)
+ else:
+ return Response({'code': 400, 'msg': '邀请数量比较符无效'})
+ except:
+ return Response({'code': 400, 'msg': '邀请数量格式错误'})
+
+ # 状态筛选
+ if status is not None and status != '':
+ try:
+ status_val = int(status)
+ if status_val not in (0, 1):
+ raise ValueError
+ qs = qs.filter(guanshi_profile__zhuangtai=status_val)
+ except:
+ return Response({'code': 400, 'msg': '状态值无效'})
+
+ # 计算总数
+ total = qs.count()
+
+ # 分页
+ offset = (page - 1) * page_size
+ users = qs.order_by('-create_time')[offset:offset + page_size]
+
+ # 构建返回数据
+ result = []
+ for user in users:
+ guanshi = user.guanshi_profile
+ boss = user.boss_profile if hasattr(user, 'boss_profile') else None
+ result.append({
+ 'yonghuid': user.yonghuid,
+ 'avatar': user.avatar or '',
+ 'nickname': boss.nickname if boss else '',
+ 'zhuangtai': guanshi.zhuangtai,
+ 'yaogingshuliang': guanshi.yaogingshuliang,
+ 'yue': str(guanshi.yue),
+ 'chongzhifenrun': str(guanshi.chongzhifenrun),
+ })
+
+ return Response({'code': 0, 'data': {'list': result, 'total': total}})
+
+
+
+
+
+
+
+
+class GetGuanliDetailView(APIView):
+ """
+ 获取管事详情
+ 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一
+ POST /houtai/hthqgsxq
+ 参数:username, yonghuid
+ 返回:
+ code:0, data: {
+ base: {...},
+ balance: {...},
+ permanent_enabled, permanent_amount,
+ custom_first_dividend: {},
+ extra_dividend_map: {},
+ all_members: []
+ }
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ yonghuid = request.data.get('yonghuid', '').strip()
+
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+ if not yonghuid:
+ return Response({'code': 400, 'msg': '缺少用户ID'})
+
+ # 权限校验(需要任一权限)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
+ if not any(p in permissions for p in required_perms):
+ return Response({'code': 403, 'msg': '您没有权限查看管事详情'})
+
+ # 查询用户主表和管事扩展表
+ try:
+ user = UserMain.objects.select_related('guanshi_profile', 'boss_profile').get(yonghuid=yonghuid)
+ guanshi = user.guanshi_profile
+ boss = user.boss_profile if hasattr(user, 'boss_profile') else None
+ except UserMain.DoesNotExist:
+ return Response({'code': 404, 'msg': '管事不存在'})
+ except UserGuanshi.DoesNotExist:
+ return Response({'code': 404, 'msg': '用户不是管事'})
+
+ # 基础信息
+ base_data = {
+ 'yonghuid': user.yonghuid,
+ 'nickname': boss.nickname if boss else '',
+ 'avatar': user.avatar or '',
+ 'dianhua': guanshi.dianhua or '',
+ 'wechat': guanshi.wechat or '',
+ 'zhuangtai': guanshi.zhuangtai,
+ 'yaoqingma': guanshi.yaoqingma,
+ 'yaoqingren': guanshi.yaoqingren,
+ 'invite_qrcode_url': guanshi.invite_qrcode_url or '',
+ 'yaogingshuliang': guanshi.yaogingshuliang,
+ 'jinrichongzhi': guanshi.jinrichongzhi,
+ 'jinyuechongzhi': guanshi.jinyuechongzhi,
+ 'chongzhifenrun': str(guanshi.chongzhifenrun),
+ 'create_time': guanshi.create_time.isoformat() if guanshi.create_time else None,
+ }
+
+ # 余额提现相关
+ balance_data = {
+ 'yue': str(guanshi.yue),
+ 'jinritixian_jine': str(guanshi.jinritixian_jine),
+ 'last_tixian_date': guanshi.last_tixian_date.isoformat() if guanshi.last_tixian_date else None,
+ 'kaioi_ewai_xiane': guanshi.kaioi_ewai_xiane,
+ 'ewai_xiane': str(guanshi.ewai_xiane),
+ }
+
+ # 永久分红(二次分红)
+ permanent_enabled = guanshi.fenghong_erci_enabled
+ permanent_amount = str(guanshi.fenghong_erci) if guanshi.fenghong_erci else '0'
+
+ # 获取所有会员(用于前端选择)
+ all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc'))
+ for m in all_members:
+ m['jiage'] = str(m['jiage'])
+ m['guanshifc'] = str(m['guanshifc'])
+
+ # 获取多次分红配置(按次数和会员组织)
+ # 查询该管事的所有多次分红记录
+ duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu')
+ custom_first = {} # 首次分红定制(cishu=1)
+ extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } }
+
+ for record in duoci_records:
+ # 获取会员信息
+ member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first()
+ if not member:
+ continue
+ member_info = {
+ 'jieshao': member.jieshao,
+ 'jiage': str(member.jiage),
+ }
+ if record.cishu == 1:
+ custom_first[record.huiyuan] = float(record.guanshi_fenhong)
+ else:
+ times = record.cishu
+ if times not in extra_map:
+ extra_map[times] = {}
+ extra_map[times][record.huiyuan] = {
+ 'guanshi_fenhong': float(record.guanshi_fenhong),
+ **member_info
+ }
+
+ # 注意:首次分红定制只存储有定制记录的会员,没有定制的会员使用默认值(前端通过会员表展示)
+ # 额外次数分红按次数返回
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'base': base_data,
+ 'balance': balance_data,
+ 'permanent_enabled': permanent_enabled,
+ 'permanent_amount': permanent_amount,
+ 'custom_first_dividend': custom_first,
+ 'extra_dividend_map': extra_map,
+ 'all_members': all_members,
+ }
+ })
+
+
+
+
+
+class UpdateGuanliView(APIView):
+ """
+ 修改管事信息(基础信息、余额、提现限额、分红策略)
+ 权限细分:
+ - 修改状态(封禁/解封)及昵称/电话/微信:需要4400a
+ - 增加余额:需要4400c
+ - 减少余额:需要4400b
+ - 修改提现限额(kaioi_ewai_xiane/ewai_xiane):需要4400e
+ - 修改分红相关(永久分红、定制首次、额外次数):需要4400f
+ POST /houtai/htxggsxx
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ yonghuid = request.data.get('yonghuid', '').strip()
+
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+ if not yonghuid:
+ return Response({'code': 400, 'msg': '缺少用户ID'})
+
+ # 权限校验(先获取权限列表)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ # 查询管事对象(不在事务外加锁,事务内加锁)
+ try:
+ user = UserMain.objects.get(yonghuid=yonghuid)
+ guanshi = user.guanshi_profile
+ boss = user.boss_profile if hasattr(user, 'boss_profile') else None
+ except UserMain.DoesNotExist:
+ return Response({'code': 404, 'msg': '用户不存在'})
+ except UserGuanshi.DoesNotExist:
+ return Response({'code': 404, 'msg': '用户不是管事'})
+
+ # 开始事务,内部使用 select_for_update
+ with transaction.atomic():
+ # 重新获取加锁对象
+ user = UserMain.objects.select_for_update().get(yonghuid=yonghuid)
+ guanshi = user.guanshi_profile
+ boss = user.boss_profile if hasattr(user, 'boss_profile') else None
+
+ # ---------- 1. 基础信息修改(需要4400a)----------
+ nickname = request.data.get('nickname')
+ dianhua = request.data.get('dianhua')
+ wechat = request.data.get('wechat')
+ zhuangtai = request.data.get('zhuangtai')
+ avatar = request.data.get('avatar')
+
+ if any(v is not None for v in [nickname, dianhua, wechat, zhuangtai, avatar]):
+ if '4400a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改管事基础信息(需要4400a)'})
+ # 处理昵称(在老板扩展表中)
+ if nickname is not None:
+ if not boss:
+ boss = UserBoss.objects.create(user=user, nickname=nickname)
+ else:
+ boss.nickname = nickname
+ boss.save()
+ # 处理其他字段
+ if dianhua is not None:
+ guanshi.dianhua = dianhua
+ if wechat is not None:
+ guanshi.wechat = wechat
+ if zhuangtai is not None:
+ try:
+ zt = int(zhuangtai)
+ if zt in (0, 1):
+ guanshi.zhuangtai = zt
+ except:
+ pass
+ if avatar is not None:
+ user.avatar = avatar
+ user.save()
+ guanshi.save()
+
+ # ---------- 2. 余额修改(加:4400c;减:4400b)----------
+ new_yue = request.data.get('yue')
+ if new_yue is not None:
+ try:
+ new_yue = Decimal(str(new_yue))
+ if new_yue < 0:
+ return Response({'code': 400, 'msg': '余额不能为负数'})
+ except:
+ return Response({'code': 400, 'msg': '余额格式错误'})
+
+ old_yue = guanshi.yue
+ if new_yue > old_yue:
+ if '4400c' not in permissions:
+ return Response({'code': 403, 'msg': '无权限增加余额(需要4400c)'})
+ elif new_yue < old_yue:
+ if '4400b' not in permissions:
+ return Response({'code': 403, 'msg': '无权限减少余额(需要4400b)'})
+ guanshi.yue = new_yue
+ guanshi.save()
+
+ # ---------- 3. 提现限额修改(需要4400e)----------
+ kaioi_ewai = request.data.get('kaioi_ewai_xiane')
+ ewai_val = request.data.get('ewai_xiane')
+ if kaioi_ewai is not None or ewai_val is not None:
+ if '4400e' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改提现限额(需要4400e)'})
+ if kaioi_ewai is not None:
+ guanshi.kaioi_ewai_xiane = bool(kaioi_ewai)
+ if ewai_val is not None:
+ try:
+ guanshi.ewai_xiane = Decimal(str(ewai_val))
+ except:
+ return Response({'code': 400, 'msg': '额外限额金额格式错误'})
+ guanshi.save()
+
+ # ---------- 4. 分红相关修改(需要4400f)----------
+ perm_enabled = request.data.get('permanent_enabled')
+ perm_amount = request.data.get('permanent_amount')
+ custom_first = request.data.get('custom_first_dividend')
+ extra_map = request.data.get('extra_dividend_map')
+
+ if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
+ if '4400f' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改分红策略(需要4400f)'})
+
+ # 4.1 永久分红
+ if perm_enabled is not None:
+ guanshi.fenghong_erci_enabled = bool(perm_enabled)
+ if perm_amount is not None:
+ try:
+ guanshi.fenghong_erci = Decimal(str(perm_amount))
+ except:
+ return Response({'code': 400, 'msg': '永久分红金额格式错误'})
+ guanshi.save()
+
+ # 4.2 首次定制分红(cishu=1)
+ if custom_first is not None and isinstance(custom_first, dict):
+ # 获取当前所有首次定制记录
+ existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1)
+ # 前端传来的定制字典 {会员ID: 金额}
+ for huiyuan_id, amount in custom_first.items():
+ try:
+ amount = Decimal(str(amount))
+ except:
+ continue
+ if amount <= 0:
+ # 金额为0或负数表示删除定制
+ DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete()
+ else:
+ DuociFenhong.objects.update_or_create(
+ yonghuid=yonghuid,
+ huiyuan=huiyuan_id,
+ cishu=1,
+ defaults={
+ 'guanshi_fenhong': amount,
+ 'zuzhang_fenhong': Decimal('0')
+ }
+ )
+ # 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
+ for record in existing_first:
+ if record.huiyuan not in custom_first:
+ record.delete()
+
+ # 4.3 额外次数分红(cishu >= 2)
+ if extra_map is not None and isinstance(extra_map, dict):
+ # 收集需要保留的记录
+ keep_set = set()
+ for times_str, members in extra_map.items():
+ try:
+ cishu = int(times_str)
+ if cishu < 2:
+ continue
+ except:
+ continue
+ if not isinstance(members, dict):
+ continue
+ for huiyuan_id, data in members.items():
+ if not isinstance(data, dict):
+ continue
+ amount = data.get('guanshi_fenhong')
+ if amount is None:
+ continue
+ try:
+ amount = Decimal(str(amount))
+ except:
+ continue
+ keep_set.add((cishu, huiyuan_id))
+ # 更新或创建
+ DuociFenhong.objects.update_or_create(
+ yonghuid=yonghuid,
+ huiyuan=huiyuan_id,
+ cishu=cishu,
+ defaults={
+ 'guanshi_fenhong': amount,
+ 'zuzhang_fenhong': Decimal('0')
+ }
+ )
+ # 删除不在 keep_set 中的额外次数记录
+ existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2)
+ for record in existing_extra:
+ if (record.cishu, record.huiyuan) not in keep_set:
+ record.delete()
+
+ return Response({'code': 0, 'msg': '修改成功'})
+
+
+
+
+
+
+
+
+
+class GetWithdrawSettingsView(APIView):
+ """
+ 获取提现设置(手续费利率、每日限额、当日已提现总额)
+ 权限:需要 5500a 或 5500b 或 5500c 任一
+ POST /houtai/hthqtxpz
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ # 需要5500a/b/c任一
+ if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
+ return Response({'code': 403, 'msg': '无权限查看提现设置'})
+
+ # 1. 手续费利率(Lilubiao:5打手/6管事/8组长/9审核官/10商家/11打手押金)
+ rate_map = {}
+ for leixing, code in [(1, '5'), (2, '6'), (3, '8'), (4, '9'), (5, '11'), (6, '10')]:
+ obj = Lilubiao.objects.filter(fadanpingtai=code).first()
+ rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0
+
+ # 1b. 订单/押金分红费率(Lilubiao:1平台订单/3商家订单/12押金管事/13商家订单管事分红)
+ order_rate_map = {}
+ for leixing, code in [(1, '1'), (3, '3'), (12, '12'), (13, '13')]:
+ obj = Lilubiao.objects.filter(fadanpingtai=code).first()
+ order_rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0
+
+ # 2. 个人每日限额 (leixing=1,2,3)
+ quota_map = {}
+ for leixing in [1, 2, 3]:
+ obj = TixianQuotaDefault.objects.filter(leixing=leixing).first()
+ quota_map[leixing] = float(obj.default_quota) if obj else 20.0
+
+ # 3. 每日总限额(提现类型1~6 → 配置表4~9)
+ total_limit_map = {}
+ for withdraw_leixing, quota_leixing in [(1, 4), (2, 5), (3, 6), (4, 7), (5, 8), (6, 9)]:
+ obj = TixianQuotaDefault.objects.filter(leixing=quota_leixing).first()
+ total_limit_map[withdraw_leixing] = float(obj.default_quota) if obj else 0.0
+
+ # 4. 当日已提现总额(按提现类型 leixing 1~6)
+ today = date.today()
+ today_totals = {i: 0.0 for i in range(1, 7)}
+ records = TixianRiTongji.objects.filter(riqi=today)
+ for rec in records:
+ if rec.leixing in today_totals:
+ today_totals[rec.leixing] = float(rec.total_amount)
+
+ # 🆕 获取提现模式(自动1 / 手动2),默认2
+ config = WithdrawConfig.objects.filter(id=1).first()
+ withdraw_mode = config.mode if config else 2
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'withdraw_mode': withdraw_mode,
+ 'rates': rate_map,
+ 'order_rates': order_rate_map,
+ 'quotas': quota_map,
+ 'total_limits': total_limit_map,
+ 'today_totals': today_totals,
+ }
+ })
+
+
+class UpdateWithdrawSettingsView(APIView):
+ """
+ 修改提现设置(权限细分):
+ - 修改打手相关(利率、个人限额、总限额)需要5500a
+ - 修改管事相关需要5500b
+ - 修改组长相关需要5500c
+ 注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算),
+ 后端不直接修改 TixianRiTongji,该表由提现流程自动累加。
+ POST /houtai/htxgtxsz
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ # 权限映射:要修改的角色需要对应的权限
+ # 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3
+ rates = request.data.get('rates', {})
+ order_rates = request.data.get('order_rates', request.data.get('commission_rates', {}))
+ quotas = request.data.get('quotas', {})
+ total_limits = request.data.get('total_limits', {})
+
+ # 角色类型映射
+ role_perms = {1: '5500a', 2: '5500b', 3: '5500c'}
+ # 利率代码映射(1-3 有水缸限额;4审核官/5打手押金/6商家余额仅费率)
+ rate_code_map = {1: '5', 2: '6', 3: '8', 4: '9', 5: '11', 6: '10'}
+ # 订单/押金分红(1平台订单/3商家订单/12押金管事/13商家订单管事分红)
+ order_rate_code_map = {1: '1', 3: '3', 12: '12', 13: '13'}
+ # 总限额:提现类型 → TixianQuotaDefault.leixing(4~9)
+ total_code_map = {1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9}
+
+ def _rate_key_present(data, role):
+ return (str(role) in data and data[str(role)] is not None) or \
+ (role in data and data[role] is not None)
+
+ with transaction.atomic():
+ # 遍历三个角色(限额相关)
+ for role in [1, 2, 3]:
+ perm_needed = role_perms[role]
+ if perm_needed not in permissions:
+ if _rate_key_present(rates, role) or \
+ (str(role) in quotas and quotas[str(role)] is not None) or \
+ (role in quotas and quotas.get(role) is not None) or \
+ (str(role) in total_limits and total_limits[str(role)] is not None) or \
+ (role in total_limits and total_limits.get(role) is not None):
+ return Response({'code': 403, 'msg': f'无权限修改{["","打手","管事","组长"][role]}相关设置(需要{perm_needed})'})
+
+ # 其他提现手续费(审核官/打手押金/商家余额):需 5500a/b/c 任一
+ extra_role_names = {4: '考核官', 5: '打手押金', 6: '商家余额'}
+ for role in [4, 5, 6]:
+ if _rate_key_present(rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')):
+ return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}提现手续费率'})
+ if (str(role) in total_limits and total_limits[str(role)] is not None) or \
+ (role in total_limits and total_limits.get(role) is not None):
+ if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
+ return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}总限额'})
+
+ # 订单/押金分红费率:需 5500a/b/c 任一
+ for role in [1, 3, 12, 13]:
+ if _rate_key_present(order_rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')):
+ order_names = {
+ 1: '平台订单打手分红', 3: '商家订单打手分红',
+ 12: '押金管事分红', 13: '管事订单分红',
+ }
+ return Response({'code': 403, 'msg': f'无权限修改{order_names[role]}费率'})
+
+ # 1. 修改手续费利率
+ for role in [1, 2, 3, 4, 5, 6]:
+ val = rates.get(str(role), rates.get(role))
+ if val is not None:
+ rate = Decimal(str(val))
+ code = rate_code_map[role]
+ obj, created = Lilubiao.objects.select_for_update().get_or_create(
+ fadanpingtai=code,
+ defaults={'lilu': rate}
+ )
+ if not created:
+ obj.lilu = rate
+ obj.save()
+
+ # 1b. 修改订单/押金分红费率
+ for role in [1, 3, 12, 13]:
+ val = order_rates.get(str(role), order_rates.get(role))
+ if val is not None:
+ rate = Decimal(str(val))
+ code = order_rate_code_map[role]
+ obj, created = Lilubiao.objects.select_for_update().get_or_create(
+ fadanpingtai=code,
+ defaults={'lilu': rate}
+ )
+ if not created:
+ obj.lilu = rate
+ obj.save()
+
+ # 2. 修改个人每日限额
+ for role in [1, 2, 3]:
+ if str(role) in quotas:
+ val = quotas[str(role)]
+ if val is not None:
+ quota = Decimal(str(val))
+ obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create(
+ leixing=role,
+ defaults={'default_quota': quota}
+ )
+ if not created:
+ obj.default_quota = quota
+ obj.save()
+
+ # 3. 修改每日总限额(1打手/2管事/3组长/4考核官/5押金/6商家)
+ for role in [1, 2, 3, 4, 5, 6]:
+ val = total_limits.get(str(role), total_limits.get(role))
+ if val is not None:
+ limit = Decimal(str(val))
+ code = total_code_map[role]
+ obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create(
+ leixing=code,
+ defaults={'default_quota': limit}
+ )
+ if not created:
+ obj.default_quota = limit
+ obj.save()
+
+ # 🆕 修改提现模式(有5500a/b/c任一即可)
+ withdraw_mode = request.data.get('withdraw_mode')
+ if withdraw_mode is not None:
+ if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
+ return Response({'code': 403, 'msg': '无权限修改提现模式'})
+ # 更新或创建 id=1 的配置
+ WithdrawConfig.objects.update_or_create(
+ id=1,
+ defaults={'mode': int(withdraw_mode)}
+ )
+
+ # 注意:TixianRiTongji 表由提现流程自动更新,此处不直接修改
+ return Response({'code': 0, 'msg': '设置修改成功'})
+
+
+
+class GetZuzhangListView(APIView):
+ """
+ 组长列表接口(分页 + 多条件筛选)
+ 路由:POST /houtai/hthqzzlb
+ 权限:需要登录,且拥有 6600a~6600e 任一权限
+ """
+ permission_classes = [IsAuthenticated]
+
+ @transaction.atomic # 保证查询的一致性(可选,对只读查询影响不大但无坏处)
+ def post(self, request):
+ # 1. 获取前端传递的 username(用于防越权)
+ frontend_username = request.data.get('username', '')
+
+ # 2. 调用公共校验方法,验证身份并获取权限列表
+ result = verify_kefu_permission(request, username_from_frontend=frontend_username)
+
+ # 重要:verify_kefu_permission 返回格式为 (user_obj, permissions_list) 或 (None, Response)
+ # 如果第二个元素是 Response 对象,说明校验失败,直接返回该 Response
+ if isinstance(result, tuple) and len(result) == 2:
+ user_obj, second = result
+ if isinstance(second, Response):
+ return second # 校验失败,直接返回错误响应
+ else:
+ permissions = second # 正常情况:permissions 是列表
+ else:
+ # 防止意外返回格式,视为失败
+ return Response({'code': 500, 'msg': '权限校验模块异常'})
+
+ # 3. 检查是否拥有组长管理所需权限(6600a ~ 6600e 任一)
+ required_perms = ['6600a', '6600b', '6600c', '6600d', '6600e']
+ if not any(perm in permissions for perm in required_perms):
+ logger.warning(f"用户 {request.user.phone} 尝试访问组长列表但缺少必要权限,已有权限: {permissions}")
+ return Response({'code': 403, 'msg': '您无权访问组长管理页面'})
+
+ # 4. 提取请求参数(分页 + 筛选)
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 20))
+ except ValueError:
+ page, page_size = 1, 20
+
+ keyword = request.data.get('keyword', '').strip()
+ status = request.data.get('status') # 1=正常, 0=禁用
+ invite_count_op = request.data.get('invite_count_op') # gte / lte
+ invite_count_value = request.data.get('invite_count_value')
+ amount_op = request.data.get('amount_op')
+ amount_value = request.data.get('amount_value')
+ commission_op = request.data.get('commission_op')
+ commission_value = request.data.get('commission_value')
+
+ # 5. 构建基础 QuerySet(预加载关联表)
+ queryset = UserZuzhang.objects.select_related('user', 'user__boss_profile').all()
+
+ # 6. 应用筛选条件(使用 ORM 安全过滤)
+ if keyword:
+ queryset = queryset.filter(
+ Q(user__yonghuid__icontains=keyword) |
+ Q(user__boss_profile__nickname__icontains=keyword)
+ )
+
+ if status is not None and status != '':
+ try:
+ queryset = queryset.filter(zhuangtai=int(status))
+ except ValueError:
+ pass
+
+ # 邀请总数筛选
+ if invite_count_value not in (None, ''):
+ try:
+ val = float(invite_count_value)
+ op = invite_count_op if invite_count_op in ('gte', 'lte') else 'gte'
+ queryset = queryset.filter(**{f'yaoqing_zongshu__{op}': val})
+ except (ValueError, TypeError):
+ pass
+
+ # 可提现金额筛选
+ if amount_value not in (None, ''):
+ try:
+ val = float(amount_value)
+ op = amount_op if amount_op in ('gte', 'lte') else 'gte'
+ queryset = queryset.filter(**{f'ketixian_jine__{op}': val})
+ except (ValueError, TypeError):
+ pass
+
+ # 分佣总额筛选
+ if commission_value not in (None, ''):
+ try:
+ val = float(commission_value)
+ op = commission_op if commission_op in ('gte', 'lte') else 'gte'
+ queryset = queryset.filter(**{f'fenyong_zonge__{op}': val})
+ except (ValueError, TypeError):
+ pass
+
+ # 7. 总数统计
+ total = queryset.count()
+
+ # 8. 分页
+ offset = (page - 1) * page_size
+ paged_queryset = queryset[offset:offset + page_size]
+
+ # 9. 构造返回数据
+ data_list = []
+ for zu in paged_queryset:
+ user = zu.user
+ nickname = ''
+ if hasattr(user, 'boss_profile') and user.boss_profile:
+ nickname = user.boss_profile.nickname or ''
+
+ data_list.append({
+ 'yonghuid': user.yonghuid,
+ 'avatar': user.avatar or '',
+ 'nickname': nickname,
+ 'fenyong_zonge': float(zu.fenyong_zonge),
+ 'ketixian_jine': float(zu.ketixian_jine),
+ 'yaoqing_zongshu': zu.yaoqing_zongshu,
+ 'zhuangtai': zu.zhuangtai,
+ })
+
+ return Response({
+ 'code': 0,
+ 'msg': 'success',
+ 'data': {
+ 'list': data_list,
+ 'total': total,
+ }
+ })
+
+
+
+
+
+
+
+
+
+
+class GetZuzhangDetailView(APIView):
+ """
+ 获取组长详情
+ 权限:6600a,6600b,6600c,6600d,6600e 任一
+ POST /houtai/hthqzzxq
+ 参数:username, yonghuid
+ 返回:
+ code:0, data: {
+ base: {...},
+ balance: {...},
+ permanent_enabled, permanent_amount,
+ custom_first_dividend: {},
+ extra_dividend_map: {},
+ all_members: []
+ }
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ yonghuid = request.data.get('yonghuid', '').strip()
+
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+ if not yonghuid:
+ return Response({'code': 400, 'msg': '缺少用户ID'})
+
+ # 权限校验(需要任一权限)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ required_perms = {'6600a', '6600b', '6600c', '6600d', '6600e'}
+ if not any(p in permissions for p in required_perms):
+ return Response({'code': 403, 'msg': '您没有权限查看组长详情'})
+
+ # 查询组长主表、扩展表、老板扩展表(昵称)
+ try:
+ user = UserMain.objects.select_related('zuzhang_profile', 'boss_profile').get(yonghuid=yonghuid)
+ zuzhang = user.zuzhang_profile
+ boss = user.boss_profile if hasattr(user, 'boss_profile') else None
+ except UserMain.DoesNotExist:
+ return Response({'code': 404, 'msg': '组长不存在'})
+ except UserZuzhang.DoesNotExist:
+ return Response({'code': 404, 'msg': '用户不是组长'})
+
+ # 基础信息
+ base_data = {
+ 'yonghuid': user.yonghuid,
+ 'nickname': boss.nickname if boss else '',
+ 'avatar': user.avatar or '',
+ 'dianhua': '', # 组长表无电话字段?组长扩展表没有电话/微信,但前端可能需要,可从UserMain关联?实际上组长表无电话字段,这里留空或从其他表获取
+ 'wechat': '',
+ 'zhuangtai': zuzhang.zhuangtai,
+ 'yaoqingma': zuzhang.yaoqingma,
+ 'yaoqingren': '', # 组长表没有邀请人字段
+ 'haibao_url': zuzhang.haibao_url or '',
+ 'yaoqing_zongshu': zuzhang.yaoqing_zongshu,
+ 'jinri_fenyong': str(zuzhang.jinri_fenyong),
+ 'jinyue_fenyong': str(zuzhang.jinyue_fenyong),
+ 'fenyong_zonge': str(zuzhang.fenyong_zonge),
+ 'create_time': zuzhang.create_time.isoformat() if zuzhang.create_time else None,
+ }
+
+ # 余额提现相关
+ balance_data = {
+ 'ketixian_jine': str(zuzhang.ketixian_jine),
+ 'jinri_tixian': str(zuzhang.jinri_tixian),
+ 'last_tixian_time': zuzhang.last_tixian_time.isoformat() if zuzhang.last_tixian_time else None,
+ 'kaioi_ewai_tixian': zuzhang.kaioi_ewai_tixian,
+ 'ewai_tixian_xiane': str(zuzhang.ewai_tixian_xiane),
+ }
+
+ # 永久分红(额外分红开关和金额)
+ permanent_enabled = zuzhang.kaioi_ewai_fenhong
+ permanent_amount = str(zuzhang.ewai_fenhong_jine) if zuzhang.ewai_fenhong_jine else '0'
+
+ # 获取所有会员(用于前端选择)
+ all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc'))
+ for m in all_members:
+ m['jiage'] = str(m['jiage'])
+ m['zuzhangfc'] = str(m['zuzhangfc'])
+
+ # 获取多次分红配置(按次数和会员组织)
+ duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu')
+ custom_first = {} # 首次分红定制(cishu=1)
+ extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } }
+
+ for record in duoci_records:
+ # 获取会员信息
+ member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first()
+ if not member:
+ continue
+ member_info = {
+ 'jieshao': member.jieshao,
+ 'jiage': str(member.jiage),
+ }
+ if record.cishu == 1:
+ custom_first[record.huiyuan] = float(record.zuzhang_fenhong)
+ else:
+ times = record.cishu
+ if times not in extra_map:
+ extra_map[times] = {}
+ extra_map[times][record.huiyuan] = {
+ 'zuzhang_fenhong': float(record.zuzhang_fenhong),
+ **member_info
+ }
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'base': base_data,
+ 'balance': balance_data,
+ 'permanent_enabled': permanent_enabled,
+ 'permanent_amount': permanent_amount,
+ 'custom_first_dividend': custom_first,
+ 'extra_dividend_map': extra_map,
+ 'all_members': all_members,
+ }
+ })
+
+
+
+
+class UpdateZuzhangView(APIView):
+ """
+ 修改组长信息(基础信息、余额、提现限额、分红策略)
+ 权限细分:
+ - 增加可提现金额:6600a
+ - 减少可提现金额:6600b
+ - 修改状态(封禁/解封):6600c
+ - 修改提现限额(kaioi_ewai_tixian/ewai_tixian_xiane):6600d
+ - 修改分红相关(永久分红、定制首次、额外次数):6600e
+ POST /houtai/htxgzzxx
+ 参数:
+ username, yonghuid,
+ nickname, avatar, dianhua, wechat, zhuangtai, # 基础
+ ketixian_jine, kaioi_ewai_tixian, ewai_tixian_xiane, # 余额提现
+ permanent_enabled, permanent_amount, # 永久分红
+ custom_first_dividend: {}, # 首次定制分红 {会员ID: 金额}
+ extra_dividend_map: {} # 额外次数分红 {次数: {会员ID: {zuzhang_fenhong}}}
+ """
+ permission_classes = []
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ yonghuid = request.data.get('yonghuid', '').strip()
+
+ if not username:
+ return Response({'code': 401, 'msg': '缺少username'})
+ if not yonghuid:
+ return Response({'code': 400, 'msg': '缺少用户ID'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ # 查询组长对象
+ try:
+ user = UserMain.objects.get(yonghuid=yonghuid)
+ zuzhang = user.zuzhang_profile
+ boss = user.boss_profile if hasattr(user, 'boss_profile') else None
+ except UserMain.DoesNotExist:
+ return Response({'code': 404, 'msg': '用户不存在'})
+ except UserZuzhang.DoesNotExist:
+ return Response({'code': 404, 'msg': '用户不是组长'})
+
+ # 获取原始数据(用于对比金额变化)
+ old_ketixian_jine = zuzhang.ketixian_jine
+
+ # 开始事务
+ with transaction.atomic():
+ # ---------- 1. 基础信息修改(需要6600c?按需求,基础信息修改通常用6600c,但为了安全,暂不校验,如有需要可加)----------
+ nickname = request.data.get('nickname', '').strip()
+ avatar = request.data.get('avatar', '').strip()
+ dianhua = request.data.get('dianhua', '').strip()
+ wechat = request.data.get('wechat', '').strip()
+ zhuangtai = request.data.get('zhuangtai')
+ # 状态修改需要6600c
+ if zhuangtai is not None:
+ if '6600c' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改组长状态(需要6600c)'})
+ try:
+ zt = int(zhuangtai)
+ if zt in (0, 1):
+ zuzhang.zhuangtai = zt
+ except:
+ pass
+ if nickname:
+ if not boss:
+ boss = UserBoss.objects.create(user=user, nickname=nickname)
+ else:
+ boss.nickname = nickname
+ boss.save()
+ if avatar:
+ user.avatar = avatar
+ user.save()
+ # 组长表没有电话/微信字段,如需存储可暂存到UserMain或忽略
+ # 这里不处理
+
+ # ---------- 2. 可提现金额修改(加:6600a;减:6600b)----------
+ new_ketixian_jine = request.data.get('ketixian_jine')
+ if new_ketixian_jine is not None:
+ try:
+ new_val = Decimal(str(new_ketixian_jine))
+ if new_val < 0:
+ return Response({'code': 400, 'msg': '可提现金额不能为负数'})
+ except:
+ return Response({'code': 400, 'msg': '金额格式错误'})
+ if new_val > old_ketixian_jine:
+ if '6600a' not in permissions:
+ return Response({'code': 403, 'msg': '无权限增加可提现金额(需要6600a)'})
+ elif new_val < old_ketixian_jine:
+ if '6600b' not in permissions:
+ return Response({'code': 403, 'msg': '无权限减少可提现金额(需要6600b)'})
+ zuzhang.ketixian_jine = new_val
+ zuzhang.save(update_fields=['ketixian_jine'])
+
+ # ---------- 3. 提现限额修改(需要6600d)----------
+ kaioi_ewai = request.data.get('kaioi_ewai_tixian')
+ ewai_val = request.data.get('ewai_tixian_xiane')
+ if kaioi_ewai is not None or ewai_val is not None:
+ if '6600d' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改提现限额(需要6600d)'})
+ if kaioi_ewai is not None:
+ zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai)
+ if ewai_val is not None:
+ try:
+ zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val))
+ except:
+ return Response({'code': 400, 'msg': '额外限额金额格式错误'})
+ zuzhang.save()
+
+ # ---------- 4. 分红相关修改(需要6600e)----------
+ perm_enabled = request.data.get('permanent_enabled')
+ perm_amount = request.data.get('permanent_amount')
+ custom_first = request.data.get('custom_first_dividend')
+ extra_map = request.data.get('extra_dividend_map')
+
+ if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
+ if '6600e' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改分红策略(需要6600e)'})
+
+ # 4.1 永久分红(额外分红开关和金额)
+ if perm_enabled is not None:
+ zuzhang.kaioi_ewai_fenhong = bool(perm_enabled)
+ if perm_amount is not None:
+ try:
+ zuzhang.ewai_fenhong_jine = Decimal(str(perm_amount))
+ except:
+ return Response({'code': 400, 'msg': '永久分红金额格式错误'})
+ zuzhang.save()
+
+ # 4.2 首次定制分红(cishu=1)
+ if custom_first is not None and isinstance(custom_first, dict):
+ existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1)
+ for huiyuan_id, amount in custom_first.items():
+ try:
+ amount = Decimal(str(amount))
+ except:
+ continue
+ if amount <= 0:
+ # 删除定制(恢复默认)
+ DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete()
+ else:
+ DuociFenhong.objects.update_or_create(
+ yonghuid=yonghuid,
+ huiyuan=huiyuan_id,
+ cishu=1,
+ defaults={
+ 'zuzhang_fenhong': amount,
+ 'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红
+ }
+ )
+ # 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
+ for record in existing_first:
+ if record.huiyuan not in custom_first:
+ record.delete()
+
+ # 4.3 额外次数分红(cishu >= 2)
+ if extra_map is not None and isinstance(extra_map, dict):
+ keep_set = set()
+ for times_str, members in extra_map.items():
+ try:
+ cishu = int(times_str)
+ if cishu < 2:
+ continue
+ except:
+ continue
+ if not isinstance(members, dict):
+ continue
+ for huiyuan_id, data in members.items():
+ if not isinstance(data, dict):
+ continue
+ amount = data.get('zuzhang_fenhong')
+ if amount is None:
+ continue
+ try:
+ amount = Decimal(str(amount))
+ except:
+ continue
+ keep_set.add((cishu, huiyuan_id))
+ DuociFenhong.objects.update_or_create(
+ yonghuid=yonghuid,
+ huiyuan=huiyuan_id,
+ cishu=cishu,
+ defaults={
+ 'zuzhang_fenhong': amount,
+ 'guanshi_fenhong': Decimal('0')
+ }
+ )
+ # 删除不在 keep_set 中的额外次数记录
+ existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2)
+ for record in existing_extra:
+ if (record.cishu, record.huiyuan) not in keep_set:
+ record.delete()
+
+ # 保存组长其他可能修改的字段(例如电话、微信没有,忽略)
+
+ return Response({'code': 0, 'msg': '修改成功'})
+
+
+
+
+class GetProductTypeZoneView(APIView):
+ """
+ 获取商品类型、专区及会员列表
+ 权限:需要拥有 7007a
+ 请求:POST /houtai/hthqsplxzq
+ 参数:{"username": "客服手机号"}
+ 返回:
+ code: 0
+ data: {
+ type_list: [{id, jieshao, tupian_url, yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, is_cross_enabled, paixu, product_count}, ...],
+ zone_list: [{id, mingzi, leixing_id, shenhezhuangtai, paixu}, ...],
+ member_list: [{huiyuan_id, jieshao, jiage}, ...]
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '7007a' not in permissions:
+ return Response({'code': 403, 'msg': '您无权访问此页面'})
+
+ # 查询所有商品类型(含专区数量)
+ types = ShangpinLeixing.objects.all().order_by('-paixu') # paixu 越大越靠前
+ type_list = []
+ for t in types:
+ # 统计该类型下的专区数量
+ zone_count = ShangpinZhuanqu.objects.filter(leixing_id=t.id).count()
+ type_list.append({
+ 'id': t.id,
+ 'jieshao': t.jieshao or '',
+ 'tupian_url': t.tupian_url or '',
+ 'yaoqiuleixing': t.yaoqiuleixing,
+ 'huiyuan_id': t.huiyuan_id or '',
+ 'yongjin': float(t.yongjin) if t.yongjin is not None else None,
+ 'shenhezhuangtai': t.shenhezhuangtai,
+ 'is_cross_enabled': t.is_cross_enabled,
+ 'paixu': t.paixu,
+ 'product_count': zone_count,
+ })
+
+ # 查询所有专区
+ zones = ShangpinZhuanqu.objects.all().order_by('-paixu')
+ zone_list = [
+ {
+ 'id': z.id,
+ 'mingzi': z.mingzi or '',
+ 'leixing_id': z.leixing_id,
+ 'shenhezhuangtai': z.shenhezhuangtai,
+ 'paixu': z.paixu,
+ }
+ for z in zones
+ ]
+
+ # 查询所有会员(只返回 id、介绍、价格)
+ members = Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage')
+ member_list = [
+ {
+ 'huiyuan_id': m['huiyuan_id'],
+ 'jieshao': m['jieshao'],
+ 'jiage': float(m['jiage']),
+ }
+ for m in members
+ ]
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'type_list': type_list,
+ 'zone_list': zone_list,
+ 'member_list': member_list,
+ }
+ })
+
+
+
+
+class ModifyProductTypeZoneView(APIView):
+ """
+ 统一修改商品类型/专区(增、删、改)
+ 权限:需要拥有 7007a
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser, MultiPartParser, FormParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ obj_type = request.data.get('type') # 'leixing' 或 'zhuanqu'
+ action = request.data.get('action') # 'add', 'update', 'delete'
+
+ if not username or not obj_type or not action:
+ return Response({'code': 400, 'msg': '缺少必要参数'})
+
+ # 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '7007a' not in permissions:
+ return Response({'code': 403, 'msg': '您无权进行此操作'})
+
+ if obj_type == 'leixing':
+ return self.handle_leixing(request, action)
+ elif obj_type == 'zhuanqu':
+ return self.handle_zhuanqu(request, action)
+ else:
+ return Response({'code': 400, 'msg': '无效的type参数'})
+
+ # ---------- 商品类型处理 ----------
+ def handle_leixing(self, request, action):
+ if action == 'add':
+ return self.add_leixing(request)
+ elif action == 'update':
+ return self.update_leixing(request)
+ elif action == 'delete':
+ return self.delete_leixing(request)
+ else:
+ return Response({'code': 400, 'msg': '无效的action'})
+
+ def add_leixing(self, request):
+ # 获取并校验字段
+ jieshao = request.data.get('jieshao', '').strip()
+ if not jieshao:
+ return Response({'code': 400, 'msg': '类型名称不能为空'})
+
+ # 强制转为整数
+ try:
+ yaoqiuleixing = int(request.data.get('yaoqiuleixing', 0))
+ if yaoqiuleixing not in [1, 2]:
+ raise ValueError
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'})
+
+ try:
+ shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1))
+ if shenhezhuangtai not in [1, 2]:
+ raise ValueError
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
+
+ is_cross_enabled = request.data.get('is_cross_enabled', False)
+ is_cross_enabled = is_cross_enabled in [True, 'true', '1', 'on']
+
+ huiyuan_id = request.data.get('huiyuan_id', '').strip() or None
+ yongjin = request.data.get('yongjin')
+ if yongjin is not None and yongjin != '':
+ try:
+ yongjin = Decimal(str(yongjin))
+ except:
+ return Response({'code': 400, 'msg': '佣金金额格式错误'})
+ else:
+ yongjin = None
+
+ # 图片校验
+ image_file = request.FILES.get('file')
+ if not image_file:
+ return Response({'code': 400, 'msg': '请上传类型图片'})
+
+ # 验证图片
+ try:
+ valid, msg = validate_image(image_file)
+ if not valid:
+ return Response({'code': 400, 'msg': msg})
+ except ImportError:
+ pass
+
+ # 上传图片
+ ext = image_file.name.split('.')[-1].lower()
+ new_filename = f"{uuid.uuid4().hex}.{ext}"
+ oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}"
+ url = upload_to_oss(image_file, oss_path)
+ if not url:
+ return Response({'code': 500, 'msg': '图片上传失败'})
+
+ # 创建类型
+ try:
+ with transaction.atomic():
+ leixing = ShangpinLeixing.objects.create(
+ jieshao=jieshao,
+ tupian_url=oss_path,
+ yaoqiuleixing=yaoqiuleixing,
+ huiyuan_id=huiyuan_id,
+ yongjin=yongjin,
+ shenhezhuangtai=shenhezhuangtai,
+ is_cross_enabled=is_cross_enabled,
+ paixu=0
+ )
+ return Response({'code': 0, 'msg': '添加成功', 'data': {'id': leixing.id}})
+ except Exception as e:
+ logger.error(f"添加类型失败: {e}", exc_info=True)
+ delete_from_oss(oss_path)
+ return Response({'code': 500, 'msg': '数据库错误'})
+
+ def update_leixing(self, request):
+ type_id = request.data.get('id')
+ if not type_id:
+ return Response({'code': 400, 'msg': '缺少类型ID'})
+ try:
+ leixing = ShangpinLeixing.objects.get(id=type_id)
+ except ShangpinLeixing.DoesNotExist:
+ return Response({'code': 404, 'msg': '类型不存在'})
+
+ changes = {}
+ # 名称
+ jieshao = request.data.get('jieshao')
+ if jieshao is not None and jieshao.strip() != leixing.jieshao:
+ changes['jieshao'] = jieshao.strip()
+
+ # 抢单要求类型(强制转整数)
+ yaoqiuleixing = request.data.get('yaoqiuleixing')
+ if yaoqiuleixing is not None:
+ try:
+ new_val = int(yaoqiuleixing)
+ if new_val not in [1, 2]:
+ raise ValueError
+ if new_val != leixing.yaoqiuleixing:
+ changes['yaoqiuleixing'] = new_val
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'})
+
+ # 会员ID
+ huiyuan_id = request.data.get('huiyuan_id', '').strip() or None
+ if huiyuan_id != leixing.huiyuan_id:
+ changes['huiyuan_id'] = huiyuan_id
+
+ # 佣金金额
+ yongjin = request.data.get('yongjin')
+ if yongjin is not None:
+ if yongjin == '':
+ new_val = None
+ else:
+ try:
+ new_val = Decimal(str(yongjin))
+ except:
+ return Response({'code': 400, 'msg': '佣金金额格式错误'})
+ if new_val != leixing.yongjin:
+ changes['yongjin'] = new_val
+
+ # 审核状态
+ shenhezhuangtai = request.data.get('shenhezhuangtai')
+ if shenhezhuangtai is not None:
+ try:
+ new_val = int(shenhezhuangtai)
+ if new_val not in [1, 2]:
+ raise ValueError
+ if new_val != leixing.shenhezhuangtai:
+ changes['shenhezhuangtai'] = new_val
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
+
+ # 跨平台互通
+ is_cross = request.data.get('is_cross_enabled')
+ if is_cross is not None:
+ new_cross = is_cross in [True, 'true', '1', 'on']
+ if new_cross != leixing.is_cross_enabled:
+ changes['is_cross_enabled'] = new_cross
+
+ # 图片处理
+ image_file = request.FILES.get('file')
+ old_image_path = leixing.tupian_url
+ if image_file:
+ try:
+ valid, msg = validate_image(image_file)
+ if not valid:
+ return Response({'code': 400, 'msg': msg})
+ except ImportError:
+ pass
+ ext = image_file.name.split('.')[-1].lower()
+ new_filename = f"{uuid.uuid4().hex}.{ext}"
+ oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}"
+ url = upload_to_oss(image_file, oss_path)
+ if not url:
+ return Response({'code': 500, 'msg': '图片上传失败'})
+ changes['tupian_url'] = oss_path
+
+ if not changes and not image_file:
+ return Response({'code': 400, 'msg': '未做任何修改'})
+
+ sync_to_products = request.data.get('sync_to_products') in [True, 'true', '1', 'on']
+ try:
+ with transaction.atomic():
+ for field, value in changes.items():
+ setattr(leixing, field, value)
+ leixing.save()
+
+ # 同步商品
+ if sync_to_products and ('yaoqiuleixing' in changes or 'huiyuan_id' in changes or 'yongjin' in changes):
+ product_updates = {}
+ if 'yaoqiuleixing' in changes:
+ product_updates['yaoqiuleixing'] = changes['yaoqiuleixing']
+ if 'huiyuan_id' in changes:
+ product_updates['huiyuan_id'] = changes['huiyuan_id']
+ if 'yongjin' in changes:
+ product_updates['yongjin'] = changes['yongjin']
+ if product_updates:
+ Shangpin.objects.filter(leixing_id=type_id).update(**product_updates)
+
+ # 删除旧图片
+ if image_file and old_image_path:
+ delete_from_oss(old_image_path)
+
+ return Response({'code': 0, 'msg': '修改成功'})
+ except Exception as e:
+ logger.error(f"更新类型失败: {e}", exc_info=True)
+ if image_file and 'oss_path' in locals():
+ delete_from_oss(oss_path)
+ return Response({'code': 500, 'msg': '修改失败'})
+
+ def delete_leixing(self, request):
+ type_id = request.data.get('id')
+ if not type_id:
+ return Response({'code': 400, 'msg': '缺少类型ID'})
+ delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on']
+
+ try:
+ leixing = ShangpinLeixing.objects.get(id=type_id)
+ except ShangpinLeixing.DoesNotExist:
+ return Response({'code': 404, 'msg': '类型不存在'})
+
+ with transaction.atomic():
+ # 删除该类型下的所有专区
+ zones = ShangpinZhuanqu.objects.filter(leixing_id=type_id)
+ if delete_products:
+ # 删除专区下的所有商品
+ for zone in zones:
+ Shangpin.objects.filter(zhuanqu_id=zone.id).delete()
+ zones.delete()
+ # 删除直接关联该类型的商品
+ Shangpin.objects.filter(leixing_id=type_id).delete()
+ else:
+ zones.delete()
+ # 仅清除商品的类型关联
+ Shangpin.objects.filter(leixing_id=type_id).update(leixing_id=None)
+
+ leixing.delete()
+
+ if leixing.tupian_url:
+ delete_from_oss(leixing.tupian_url)
+
+ return Response({'code': 0, 'msg': '删除成功'})
+
+ # ---------- 商品专区处理 ----------
+ def handle_zhuanqu(self, request, action):
+ if action == 'add':
+ return self.add_zhuanqu(request)
+ elif action == 'update':
+ return self.update_zhuanqu(request)
+ elif action == 'delete':
+ return self.delete_zhuanqu(request)
+ else:
+ return Response({'code': 400, 'msg': '无效的action'})
+
+ def add_zhuanqu(self, request):
+ mingzi = request.data.get('mingzi', '').strip()
+ leixing_id = request.data.get('leixing_id')
+ if not mingzi:
+ return Response({'code': 400, 'msg': '专区名称不能为空'})
+ if not leixing_id:
+ return Response({'code': 400, 'msg': '请选择所属类型'})
+
+ try:
+ leixing_id = int(leixing_id)
+ if not ShangpinLeixing.objects.filter(id=leixing_id).exists():
+ return Response({'code': 404, 'msg': '所属类型不存在'})
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '所属类型ID无效'})
+
+ try:
+ shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1))
+ if shenhezhuangtai not in [1, 2]:
+ raise ValueError
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
+
+ try:
+ with transaction.atomic():
+ zhuanqu = ShangpinZhuanqu.objects.create(
+ mingzi=mingzi,
+ leixing_id=leixing_id,
+ shenhezhuangtai=shenhezhuangtai,
+ paixu=0
+ )
+ return Response({'code': 0, 'msg': '添加成功', 'data': {'id': zhuanqu.id}})
+ except Exception as e:
+ logger.error(f"添加专区失败: {e}", exc_info=True)
+ return Response({'code': 500, 'msg': '数据库错误'})
+
+ def update_zhuanqu(self, request):
+ zone_id = request.data.get('id')
+ if not zone_id:
+ return Response({'code': 400, 'msg': '缺少专区ID'})
+ try:
+ zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id)
+ except ShangpinZhuanqu.DoesNotExist:
+ return Response({'code': 404, 'msg': '专区不存在'})
+
+ changes = {}
+ mingzi = request.data.get('mingzi')
+ if mingzi is not None and mingzi.strip() != zhuanqu.mingzi:
+ changes['mingzi'] = mingzi.strip()
+
+ leixing_id = request.data.get('leixing_id')
+ if leixing_id is not None:
+ try:
+ new_id = int(leixing_id)
+ if not ShangpinLeixing.objects.filter(id=new_id).exists():
+ return Response({'code': 404, 'msg': '新所属类型不存在'})
+ if new_id != zhuanqu.leixing_id:
+ changes['leixing_id'] = new_id
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '所属类型ID无效'})
+
+ shenhezhuangtai = request.data.get('shenhezhuangtai')
+ if shenhezhuangtai is not None:
+ try:
+ new_val = int(shenhezhuangtai)
+ if new_val not in [1, 2]:
+ raise ValueError
+ if new_val != zhuanqu.shenhezhuangtai:
+ changes['shenhezhuangtai'] = new_val
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
+
+ if not changes:
+ return Response({'code': 400, 'msg': '未做任何修改'})
+
+ try:
+ with transaction.atomic():
+ for field, value in changes.items():
+ setattr(zhuanqu, field, value)
+ zhuanqu.save()
+ return Response({'code': 0, 'msg': '修改成功'})
+ except Exception as e:
+ logger.error(f"更新专区失败: {e}", exc_info=True)
+ return Response({'code': 500, 'msg': '修改失败'})
+
+ def delete_zhuanqu(self, request):
+ zone_id = request.data.get('id')
+ if not zone_id:
+ return Response({'code': 400, 'msg': '缺少专区ID'})
+ delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on']
+
+ try:
+ zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id)
+ except ShangpinZhuanqu.DoesNotExist:
+ return Response({'code': 404, 'msg': '专区不存在'})
+
+ with transaction.atomic():
+ if delete_products:
+ Shangpin.objects.filter(zhuanqu_id=zone_id).delete()
+ else:
+ Shangpin.objects.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None)
+ zhuanqu.delete()
+
+ return Response({'code': 0, 'msg': '删除成功'})
+
+
+# 继续在 houtai/views_product.py 中添加
+class GetRateView(APIView):
+ """
+ 获取打手分成费率(平台订单和商家订单)
+ 权限:需要拥有 7007b
+ 请求:POST /houtai/hthqfhpz
+ 参数:{"username": "客服手机号"}
+ 返回:{"code":0, "data": {"platform_rate": xx, "merchant_rate": xx}}
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '7007b' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看费率'})
+
+ try:
+ platform_rate_obj = Lilubiao.objects.get(fadanpingtai='1')
+ merchant_rate_obj = Lilubiao.objects.get(fadanpingtai='3')
+ except Lilubiao.DoesNotExist as e:
+ logger.error(f"费率记录缺失: {e}")
+ return Response({'code': 500, 'msg': '费率配置缺失'})
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'platform_rate': float(platform_rate_obj.lilu) if platform_rate_obj.lilu else 0,
+ 'merchant_rate': float(merchant_rate_obj.lilu) if merchant_rate_obj.lilu else 0,
+ }
+ })
+
+
+class ModifyRateView(APIView):
+ """
+ 修改打手分成费率
+ 权限:需要拥有 7007b
+ 请求:POST /houtai/htxgfl
+ 参数:{"username": "客服手机号", "type": "platform" / "merchant", "value": 费率(百分比数字)}
+ 返回:{"code":0, "msg":"修改成功"}
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ rate_type = request.data.get('type')
+ value = request.data.get('value')
+
+ if not username or not rate_type or value is None:
+ return Response({'code': 400, 'msg': '缺少参数'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '7007b' not in permissions:
+ return Response({'code': 403, 'msg': '您无权修改费率'})
+
+ try:
+ rate_value = Decimal(str(value))
+ except:
+ return Response({'code': 400, 'msg': '费率值格式错误'})
+
+
+ if rate_type == 'platform':
+ fadanpingtai = '1'
+ elif rate_type == 'merchant':
+ fadanpingtai = '3'
+ else:
+ return Response({'code': 400, 'msg': '无效的type参数'})
+
+ try:
+ with transaction.atomic():
+ rate_obj = Lilubiao.objects.select_for_update().get(fadanpingtai=fadanpingtai)
+ rate_obj.lilu = rate_value
+ rate_obj.save()
+ except Lilubiao.DoesNotExist:
+ return Response({'code': 404, 'msg': '费率记录不存在'})
+ except Exception as e:
+ logger.error(f"修改费率失败: {e}", exc_info=True)
+ return Response({'code': 500, 'msg': '修改失败'})
+
+ return Response({'code': 0, 'msg': '修改成功'})
+
+
+
+
+
+
+# 权限码(根据你要求,使用 '8080a')
+REQUIRED_PERMISSION = '8080a'
+class PopupNoticeListAPIView(APIView):
+ """
+ 获取所有弹窗配置(页面 + 弹窗 + 图片)
+ URL: POST /houtai/hthqtcxx
+ 请求参数: { "username": "客服账号" }
+ 响应格式: { "code": 0, "data": { "pages": [...] } }
+ 注意:所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段区分
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 获取前端传递的 username(用于防越权)
+ username_frontend = request.data.get('username')
+ # 2. 调用公共权限验证方法,返回 (客服对象, 权限列表)
+ kefu, permissions = verify_kefu_permission(request, username_frontend)
+ if kefu is None:
+ # 身份验证失败,返回业务码 403,但 HTTP 状态码仍是 200
+ return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
+
+ # 3. 检查是否拥有所需权限码 '8080a'
+ if REQUIRED_PERMISSION not in permissions:
+ return Response({'code': 403, 'msg': '您没有权限访问弹窗管理功能'})
+
+ # 4. 查询所有页面,并预加载关联的弹窗和图片(避免N+1查询)
+ pages = PopupPage.objects.all().prefetch_related('popups__images').order_by('id')
+
+ # 5. 序列化输出
+ serializer = PopupPageSerializer(pages, many=True)
+ return Response({'code': 0, 'data': {'pages': serializer.data}})
+
+
+class PopupNoticeModifyAPIView(APIView):
+ """
+ 统一修改接口:支持页面、弹窗、图片的增删改
+ URL: POST /houtai/htxgtcxx
+ 请求中必须包含 action 字段,用于区分操作类型。
+ 所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段体现。
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # ---------- 1. 权限验证 ----------
+ username_frontend = request.data.get('username')
+ kefu, permissions = verify_kefu_permission(request, username_frontend)
+ if kefu is None:
+ return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
+ if REQUIRED_PERMISSION not in permissions:
+ return Response({'code': 403, 'msg': '您没有权限操作弹窗管理'})
+
+ # ---------- 2. 获取 action 并分发 ----------
+ action = request.data.get('action')
+ if not action:
+ return Response({'code': 400, 'msg': '缺少action参数'})
+
+ try:
+ if action == 'add_page':
+ return self.add_page(request)
+ elif action == 'update_page':
+ return self.update_page(request)
+ elif action == 'delete_page':
+ return self.delete_page(request)
+ elif action == 'add_popup':
+ return self.add_popup(request)
+ elif action == 'update_popup':
+ return self.update_popup(request)
+ elif action == 'delete_popup':
+ return self.delete_popup(request)
+ elif action == 'add_image':
+ return self.add_image(request)
+ elif action == 'update_image':
+ return self.update_image(request)
+ elif action == 'delete_image':
+ return self.delete_image(request)
+ else:
+ return Response({'code': 400, 'msg': f'未知的action: {action}'})
+ except Exception as e:
+ # 捕获未预期的异常,返回服务器错误
+ traceback.print_exc()
+ return Response({'code': 500, 'msg': f'服务器错误: {str(e)}'})
+
+ # ==================== 页面相关操作 ====================
+
+ def add_page(self, request):
+ """添加新页面"""
+ data = request.data
+ # 必填字段校验
+ if not data.get('page_key') or not data.get('name'):
+ return Response({'code': 400, 'msg': '缺少 page_key 或 name'})
+ # 唯一性校验
+ if PopupPage.objects.filter(page_key=data['page_key']).exists():
+ return Response({'code': 400, 'msg': '页面标识已存在'})
+
+ page = PopupPage.objects.create(
+ page_key=data['page_key'],
+ name=data['name'],
+ description=data.get('description', ''),
+ is_active=data.get('is_active', True),
+ ignore_user_mute=data.get('ignore_user_mute', False)
+ )
+ return Response({'code': 0, 'msg': '添加成功', 'data': {'id': page.id}})
+
+ def update_page(self, request):
+ """更新页面信息"""
+ page_id = request.data.get('id')
+ if not page_id:
+ return Response({'code': 400, 'msg': '缺少页面id'})
+ try:
+ page = PopupPage.objects.get(id=page_id)
+ except PopupPage.DoesNotExist:
+ return Response({'code': 404, 'msg': '页面不存在'})
+
+ # 更新允许修改的字段
+ page.page_key = request.data.get('page_key', page.page_key)
+ page.name = request.data.get('name', page.name)
+ page.description = request.data.get('description', page.description)
+ page.is_active = request.data.get('is_active', page.is_active)
+ page.ignore_user_mute = request.data.get('ignore_user_mute', page.ignore_user_mute)
+ page.save()
+ return Response({'code': 0, 'msg': '更新成功'})
+
+ def delete_page(self, request):
+ """删除页面,可级联删除其下所有弹窗和图片"""
+ page_id = request.data.get('id')
+ cascade = request.data.get('cascade_delete_popups', True) # 默认级联
+ if not page_id:
+ return Response({'code': 400, 'msg': '缺少页面id'})
+ try:
+ page = PopupPage.objects.get(id=page_id)
+ except PopupPage.DoesNotExist:
+ return Response({'code': 404, 'msg': '页面不存在'})
+
+ if cascade:
+ # 级联删除:使用事务保证原子性
+ with transaction.atomic():
+ popups = page.popups.all()
+ for popup in popups:
+ # 删除弹窗下的所有图片文件
+ for img in popup.images.all():
+ if img.image_url:
+ delete_from_oss(img.image_url)
+ popups.delete()
+ page.delete()
+ else:
+ # 非级联删除:如果页面下还有弹窗,则拒绝删除
+ if page.popups.exists():
+ return Response({'code': 400, 'msg': '该页面下存在弹窗,请先删除弹窗或使用级联删除'})
+ page.delete()
+
+ return Response({'code': 0, 'msg': '删除成功'})
+
+ # ==================== 弹窗相关操作 ====================
+
+ def add_popup(self, request):
+ """为指定页面添加新弹窗"""
+ data = request.data
+ page_id = data.get('page_id')
+ popup_id = data.get('popup_id')
+ if not page_id or not popup_id:
+ return Response({'code': 400, 'msg': '缺少 page_id 或 popup_id'})
+ try:
+ page = PopupPage.objects.get(id=page_id)
+ except PopupPage.DoesNotExist:
+ return Response({'code': 404, 'msg': '页面不存在'})
+
+ # 同一页面下 popup_id 必须唯一
+ if PopupConfig.objects.filter(page=page, popup_id=popup_id).exists():
+ return Response({'code': 400, 'msg': '该页面下已存在相同的弹窗ID'})
+
+ popup = PopupConfig.objects.create(
+ page=page,
+ popup_id=popup_id,
+ title=data.get('title', ''),
+ description=data.get('description', ''),
+ strategy_type=data.get('strategy_type', 'daily'),
+ max_count=data.get('max_count', 1),
+ reset_interval=data.get('reset_interval', 'day'),
+ duration=data.get('duration', 0),
+ force_even_muted=data.get('force_even_muted', False),
+ sort_order=data.get('sort_order', 0),
+ is_active=data.get('is_active', True),
+ start_time=data.get('start_time') or None,
+ end_time=data.get('end_time') or None
+ )
+ return Response({'code': 0, 'msg': '添加成功', 'data': {'id': popup.id}})
+
+ def update_popup(self, request):
+ """更新弹窗信息"""
+ popup_id = request.data.get('id')
+ if not popup_id:
+ return Response({'code': 400, 'msg': '缺少弹窗id'})
+ try:
+ popup = PopupConfig.objects.get(id=popup_id)
+ except PopupConfig.DoesNotExist:
+ return Response({'code': 404, 'msg': '弹窗不存在'})
+
+ # 更新弹窗字段
+ popup.popup_id = request.data.get('popup_id', popup.popup_id)
+ popup.title = request.data.get('title', popup.title)
+ popup.description = request.data.get('description', popup.description)
+ popup.strategy_type = request.data.get('strategy_type', popup.strategy_type)
+ popup.max_count = request.data.get('max_count', popup.max_count)
+ popup.reset_interval = request.data.get('reset_interval', popup.reset_interval)
+ popup.duration = request.data.get('duration', popup.duration)
+ popup.force_even_muted = request.data.get('force_even_muted', popup.force_even_muted)
+ popup.sort_order = request.data.get('sort_order', popup.sort_order)
+ popup.is_active = request.data.get('is_active', popup.is_active)
+ popup.start_time = request.data.get('start_time') or None
+ popup.end_time = request.data.get('end_time') or None
+ popup.save()
+ return Response({'code': 0, 'msg': '更新成功'})
+
+ def delete_popup(self, request):
+ """删除弹窗,同时删除其下所有图片(包括OSS文件)"""
+ popup_id = request.data.get('id')
+ if not popup_id:
+ return Response({'code': 400, 'msg': '缺少弹窗id'})
+ try:
+ popup = PopupConfig.objects.get(id=popup_id)
+ except PopupConfig.DoesNotExist:
+ return Response({'code': 404, 'msg': '弹窗不存在'})
+
+ with transaction.atomic():
+ for img in popup.images.all():
+ if img.image_url:
+ delete_from_oss(img.image_url)
+ popup.images.all().delete()
+ popup.delete()
+ return Response({'code': 0, 'msg': '删除成功'})
+
+ # ==================== 图片相关操作 ====================
+
+ def _upload_image_file(self, file_obj, page_key):
+ """
+ 内部方法:将图片上传到OSS指定目录
+ 目录: a_long/tanchuang/tanchuang/
+ 文件名: {page_key}_{时间戳}_{uuid8}.{ext}
+ 返回: 相对路径(例如 a_long/tanchuang/tanchuang/dashouzhongxin_20250419120000_abc12345.jpg)
+ """
+ # 验证图片格式和大小
+ valid, msg = validate_image(file_obj)
+ if not valid:
+ raise ValueError(msg)
+
+ # 生成唯一文件名
+ ext = os.path.splitext(file_obj.name)[1].lower()
+ timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
+ unique_id = str(uuid.uuid4())[:8]
+ filename = f"{page_key}_{timestamp}_{unique_id}{ext}"
+ relative_path = f"a_long/tanchuang/tanchuang/{filename}"
+
+ # 上传到OSS
+ url = upload_to_oss(file_obj, relative_path)
+ if not url:
+ raise ValueError("图片上传失败")
+ return relative_path
+
+ def add_image(self, request):
+ """为指定弹窗添加图片(支持上传文件)"""
+ popup_id = request.data.get('popup_id')
+ if not popup_id:
+ return Response({'code': 400, 'msg': '缺少 popup_id'})
+ try:
+ popup = PopupConfig.objects.get(id=popup_id)
+ except PopupConfig.DoesNotExist:
+ return Response({'code': 404, 'msg': '弹窗不存在'})
+
+ # 获取上传的文件
+ file_obj = request.FILES.get('file')
+ if not file_obj:
+ return Response({'code': 400, 'msg': '未提供图片文件'})
+
+ try:
+ page_key = popup.page.page_key
+ relative_path = self._upload_image_file(file_obj, page_key)
+ except ValueError as e:
+ return Response({'code': 400, 'msg': str(e)})
+
+ # 保存图片记录
+ image = PopupImage.objects.create(
+ popup_config=popup,
+ image_url=relative_path,
+ text=request.data.get('text', ''),
+ sort_order=request.data.get('sort_order', 0)
+ )
+ return Response({'code': 0, 'msg': '添加成功', 'data': {'id': image.id}})
+
+ def update_image(self, request):
+ """更新图片信息(可替换图片文件、修改文字、排序)"""
+ image_id = request.data.get('id')
+ if not image_id:
+ return Response({'code': 400, 'msg': '缺少图片id'})
+ try:
+ image = PopupImage.objects.get(id=image_id)
+ except PopupImage.DoesNotExist:
+ return Response({'code': 404, 'msg': '图片不存在'})
+
+ # 更新文字和排序
+ image.text = request.data.get('text', image.text)
+ image.sort_order = request.data.get('sort_order', image.sort_order)
+
+ # 如果提供了新图片文件,则替换
+ file_obj = request.FILES.get('file')
+ if file_obj:
+ try:
+ page_key = image.popup_config.page.page_key
+ new_path = self._upload_image_file(file_obj, page_key)
+ except ValueError as e:
+ return Response({'code': 400, 'msg': str(e)})
+ # 删除旧图片
+ if image.image_url:
+ delete_from_oss(image.image_url)
+ image.image_url = new_path
+
+ image.save()
+ return Response({'code': 0, 'msg': '更新成功'})
+
+ def delete_image(self, request):
+ """删除图片(同时删除OSS文件)"""
+ image_id = request.data.get('id')
+ if not image_id:
+ return Response({'code': 400, 'msg': '缺少图片id'})
+ try:
+ image = PopupImage.objects.get(id=image_id)
+ except PopupImage.DoesNotExist:
+ return Response({'code': 404, 'msg': '图片不存在'})
+
+ if image.image_url:
+ delete_from_oss(image.image_url)
+ image.delete()
+ return Response({'code': 0, 'msg': '删除成功'})
+
+
+
+class ShopListView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 调用公共权限验证(与您给的示例完全一致)
+ username_frontend = request.data.get('username')
+ kefu, permissions = verify_kefu_permission(request, username_frontend)
+ if kefu is None:
+ return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
+
+ # 2. 检查是否拥有所需权限(999a~999e 任意一个)
+ required_perms = {'999a', '999b', '999c', '999d', '999e'}
+ if not required_perms.intersection(set(permissions)):
+ return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'})
+
+ # 3. 解析请求参数
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 10))
+ dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip()
+ zhuangtai = request.data.get('zhuangtai')
+ lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip()
+ user_id = request.data.get('user_id', '').strip()
+ ketixian_yue_min = request.data.get('ketixian_yue_min')
+ ketixian_yue_max = request.data.get('ketixian_yue_max')
+ zhengqu_zonge_min = request.data.get('zhengqu_zonge_min')
+ zhengqu_zonge_max = request.data.get('zhengqu_zonge_max')
+
+ # 4. 构建查询集(先查店铺,预加载关联凭证)
+ queryset = Dianpu.objects.select_related('pingzheng').all()
+
+ if dianpu_mingcheng:
+ queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng)
+ if zhuangtai is not None:
+ queryset = queryset.filter(zhuangtai=zhuangtai)
+ if lianxi_dianhua:
+ queryset = queryset.filter(lianxi_dianhua__icontains=lianxi_dianhua)
+ if user_id:
+ queryset = queryset.filter(pingzheng__yonghu=user_id)
+
+ # 金额范围过滤
+ if ketixian_yue_min is not None:
+ queryset = queryset.filter(ketixian_yue__gte=ketixian_yue_min)
+ if ketixian_yue_max is not None:
+ queryset = queryset.filter(ketixian_yue__lte=ketixian_yue_max)
+ if zhengqu_zonge_min is not None:
+ queryset = queryset.filter(zhengqu_zonge__gte=zhengqu_zonge_min)
+ if zhengqu_zonge_max is not None:
+ queryset = queryset.filter(zhengqu_zonge__lte=zhengqu_zonge_max)
+
+ # 5. 分页
+ paginator = Paginator(queryset, page_size)
+ page_obj = paginator.get_page(page)
+
+ # 6. 构造返回数据
+ shop_list = []
+ for dianpu in page_obj:
+ pingzheng = dianpu.pingzheng
+ shop_list.append({
+ 'id': dianpu.id,
+ 'dianpu_mingcheng': dianpu.dianpu_mingcheng,
+ 'dianpu_touxiang': dianpu.dianpu_touxiang or '',
+ 'lianxi_dianhua': dianpu.lianxi_dianhua or '',
+ 'weixinhao': dianpu.weixinhao or '',
+ 'zhuangtai': dianpu.zhuangtai,
+ 'bangding_yonghushu': dianpu.bangding_yonghushu,
+ 'erweima_url': dianpu.erweima_url or '',
+ 'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng,
+ 'shouyi_choucheng_feilv': dianpu.shouyi_choucheng_feilv or 0,
+ 'ketixian_yue': dianpu.ketixian_yue,
+ 'zhengqu_zonge': dianpu.zhengqu_zonge,
+ 'meiri_tixian_xiane': dianpu.meiri_tixian_xiane or 0,
+ 'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane,
+ 'pingzheng__yonghu': pingzheng.yonghu, # 用户ID
+ 'pingzheng__zhanghao': pingzheng.zhanghao, # 登录账号
+ })
+
+ # 获取默认配置(ID=1,不存在则创建)
+ default_config, _ = DianpuMorenPeizhi.objects.get_or_create(id=1, defaults={
+ 'moren_choucheng_feilv': 0.1,
+ 'moren_tixian_xiane': 1000.00
+ })
+
+ return Response({
+ 'code': 0,
+ 'msg': 'success',
+ 'data': {
+ 'list': shop_list,
+ 'total': paginator.count,
+ 'default_config': {
+ 'moren_choucheng_feilv': default_config.moren_choucheng_feilv,
+ 'moren_tixian_xiane': default_config.moren_tixian_xiane,
+ }
+ }
+ })
+
+
+class ShopModifyView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 身份验证
+ username_frontend = request.data.get('username')
+ kefu, permissions = verify_kefu_permission(request, username_frontend)
+ if kefu is None:
+ return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
+
+ # 转换权限为集合方便判断
+ user_perms = set(permissions)
+ action = request.data.get('action', '')
+
+ # 2. 根据 action 分派任务
+ if action == 'update_dianpu':
+ return self._update_dianpu(request, user_perms)
+ elif action == 'add_dianpu':
+ return self._add_dianpu(request, user_perms)
+ elif action == 'update_default':
+ return self._update_default(request, user_perms)
+ else:
+ return Response({'code': 400, 'msg': '未知操作类型'})
+
+ def _update_dianpu(self, request, user_perms):
+ """修改店铺信息,精确权限控制"""
+ dianpu_id = request.data.get('dianpu_id')
+ if not dianpu_id:
+ return Response({'code': 400, 'msg': '缺少店铺ID'})
+
+ try:
+ with transaction.atomic():
+ dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id)
+
+ # 基础信息修改 (999e)
+ if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']):
+ if '999e' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限修改店铺基本信息(999e)'})
+ if 'dianpu_mingcheng' in request.data:
+ dianpu.dianpu_mingcheng = request.data['dianpu_mingcheng']
+ if 'lianxi_dianhua' in request.data:
+ dianpu.lianxi_dianhua = request.data['lianxi_dianhua']
+ if 'weixinhao' in request.data:
+ dianpu.weixinhao = request.data['weixinhao']
+
+ # 店铺状态 (999a)
+ if 'zhuangtai' in request.data:
+ if '999a' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限修改店铺状态(999a)'})
+ new_status = int(request.data['zhuangtai'])
+ if new_status not in (0, 1):
+ return Response({'code': 400, 'msg': '店铺状态无效'})
+ dianpu.zhuangtai = new_status
+
+ # 可提现余额 (999b)
+ if 'ketixian_yue' in request.data:
+ if '999b' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限修改可提现余额(999b)'})
+ dianpu.ketixian_yue = request.data['ketixian_yue']
+
+ # 提现限额相关 (999c)
+ if 'kaiqi_meiri_xiane' in request.data or 'meiri_tixian_xiane' in request.data:
+ if '999c' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限修改提现限额配置(999c)'})
+ if 'kaiqi_meiri_xiane' in request.data:
+ dianpu.kaiqi_meiri_xiane = request.data['kaiqi_meiri_xiane']
+ if 'meiri_tixian_xiane' in request.data:
+ dianpu.meiri_tixian_xiane = request.data['meiri_tixian_xiane']
+
+ # 抽成相关 (999d)
+ if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data:
+ if '999d' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限修改抽成配置(999d)'})
+ if 'kaiqi_shouyi_choucheng' in request.data:
+ dianpu.kaiqi_shouyi_choucheng = request.data['kaiqi_shouyi_choucheng']
+ if 'shouyi_choucheng_feilv' in request.data:
+ dianpu.shouyi_choucheng_feilv = request.data['shouyi_choucheng_feilv']
+
+ dianpu.save() # Django 会自动判断更新哪些字段
+ return Response({'code': 0, 'msg': '店铺信息更新成功'})
+
+ except Dianpu.DoesNotExist:
+ return Response({'code': 404, 'msg': '店铺不存在'})
+ except Exception as e:
+ logger.exception("修改店铺失败")
+ return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
+
+ def _add_dianpu(self, request, user_perms):
+ """添加店铺,需要权限 999b"""
+ if '999b' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限添加店铺(999b)'})
+
+ yonghu_id = request.data.get('yonghu_id')
+ zhanghao = request.data.get('zhanghao')
+ mima = request.data.get('mima')
+ dianpu_mingcheng = request.data.get('dianpu_mingcheng')
+
+ if not all([yonghu_id, zhanghao, mima, dianpu_mingcheng]):
+ return Response({'code': 400, 'msg': '必填字段缺失(用户ID、账号、密码、店铺名称)'})
+
+ # 检查用户是否存在
+ if not UserMain.objects.filter(yonghuid=yonghu_id).exists():
+ return Response({'code': 404, 'msg': '用户不存在'})
+
+ # 检查凭证是否已存在
+ if YonghuPingzheng.objects.filter(zhanghao=zhanghao).exists():
+ return Response({'code': 400, 'msg': '登录账号已存在'})
+ if YonghuPingzheng.objects.filter(yonghu=yonghu_id).exists():
+ return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'})
+
+ try:
+ with transaction.atomic():
+ pingzheng = YonghuPingzheng.objects.create(
+ yonghu=yonghu_id,
+ zhanghao=zhanghao,
+ mima=mima,
+ is_active=True
+ )
+ dianpu = Dianpu.objects.create(
+ pingzheng=pingzheng,
+ dianpu_mingcheng=dianpu_mingcheng,
+ lianxi_dianhua=request.data.get('lianxi_dianhua', ''),
+ weixinhao=request.data.get('weixinhao', ''),
+ kaiqi_shouyi_choucheng=request.data.get('kaiqi_shouyi_choucheng', False),
+ shouyi_choucheng_feilv=request.data.get('shouyi_choucheng_feilv', 0),
+ kaiqi_meiri_xiane=request.data.get('kaiqi_meiri_xiane', False),
+ meiri_tixian_xiane=request.data.get('meiri_tixian_xiane', 0),
+ )
+ return Response({'code': 0, 'msg': '店铺创建成功', 'dianpu_id': dianpu.id})
+ except IntegrityError as e:
+ logger.error("创建店铺违反唯一性约束: %s", e)
+ return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'})
+ except Exception as e:
+ logger.exception("创建店铺失败")
+ return Response({'code': 500, 'msg': '创建失败,请稍后重试'})
+
+ def _update_default(self, request, user_perms):
+ """修改默认配置,按修改内容校验权限"""
+ feilv = request.data.get('moren_choucheng_feilv')
+ xiane = request.data.get('moren_tixian_xiane')
+ modify_commission = feilv is not None
+ modify_withdraw = xiane is not None
+
+ if modify_commission and '999d' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'})
+ if modify_withdraw and '999c' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'})
+
+ try:
+ config, _ = DianpuMorenPeizhi.objects.get_or_create(id=1)
+ if modify_commission:
+ config.moren_choucheng_feilv = feilv
+ if modify_withdraw:
+ config.moren_tixian_xiane = xiane
+ config.save(update_fields=(['moren_choucheng_feilv'] if modify_commission else []) +
+ (['moren_tixian_xiane'] if modify_withdraw else []))
+ return Response({'code': 0, 'msg': '默认配置更新成功'})
+ except Exception as e:
+ logger.exception("更新默认配置失败")
+ return Response({'code': 500, 'msg': '更新失败'})
+
+
+
+
+
+
+
+
+# ==================== 1. 公共商品类型 + 审核模式 ====================
+class ShopPublicTypeAndAuditView(APIView):
+ """
+ 返回:①正常状态的公共商品类型列表 ②全局商品审核模式开关
+ 前端请求路径:/houtai/htdphqsplx
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # ---------- 身份与权限校验 ----------
+ username = request.data.get('username')
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
+
+ # 拥有 1199ab/1199abc/1199abd 任一个即可访问
+ if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
+ return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'})
+
+ # ---------- 查询公共商品类型(仅正常审核状态的) ----------
+ try:
+ # shenhezhuangtai = 1 表示正常 (根据模型默认值)
+ types_qs = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values(
+ 'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', 'is_cross_enabled'
+ )
+ type_list = []
+ for t in types_qs:
+ type_list.append({
+ 'id': t['id'],
+ 'jieshao': t['jieshao'] or '',
+ 'tupian_url': t['tupian_url'] or '',
+ 'yaoqiuleixing': t['yaoqiuleixing'],
+ 'huiyuan_id': t['huiyuan_id'],
+ 'yongjin': t['yongjin'],
+ 'is_cross_enabled': t['is_cross_enabled'],
+ })
+ except Exception as e:
+ logger.exception("查询公共商品类型失败")
+ return Response({'code': 500, 'msg': '服务器错误,请稍后重试'})
+
+ # ---------- 获取审核模式配置 ----------
+ audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first()
+ kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'public_types': type_list,
+ 'kaiqi_shenhe': kaiqi_shenhe
+ }
+ })
+
+
+# ==================== 2. 店铺列表(支持搜索、筛选、分页) ====================
+class ShopListForProductView(APIView):
+ """
+ 返回店铺列表,用于商品管理页面选择店铺。
+ 支持:关键词搜索(店铺ID/名称/用户ID)、店铺状态筛选、分页。
+ 前端请求路径:/houtai/htdphqdpys
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ username = request.data.get('username')
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return Response({'code': 403, 'msg': '身份验证失败'})
+ if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
+ return Response({'code': 403, 'msg': '无权访问店铺列表'})
+
+ # ---------- 安全解析分页参数 ----------
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 10))
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '分页参数格式错误'})
+
+ # 搜索关键词(字符串,ORM 自动防注入)
+ keyword = str(request.data.get('keyword', '')).strip()
+ # 店铺状态筛选(1正常,0封禁,不传则全部)
+ zhuangtai = request.data.get('zhuangtai')
+
+ # ---------- 构建查询集(关联凭证表获取用户ID) ----------
+ shops = Dianpu.objects.select_related('pingzheng').all()
+
+ # 状态筛选:只接受 0 或 1
+ if zhuangtai is not None:
+ try:
+ zhuangtai = int(zhuangtai)
+ if zhuangtai in [0, 1]:
+ shops = shops.filter(zhuangtai=zhuangtai)
+ else:
+ return Response({'code': 400, 'msg': '状态参数只能为0或1'})
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '状态参数必须为整数'})
+
+ # 关键词搜索(店铺ID/名称/用户ID)
+ if keyword:
+ shops = shops.filter(
+ models.Q(id__icontains=keyword) |
+ models.Q(dianpu_mingcheng__icontains=keyword) |
+ models.Q(pingzheng__yonghu__icontains=keyword)
+ )
+
+ # ---------- 分页 ----------
+ paginator = Paginator(shops, page_size)
+ page_obj = paginator.get_page(page)
+
+ shop_list = []
+ for shop in page_obj:
+ shop_list.append({
+ 'id': shop.id,
+ 'dianpu_mingcheng': shop.dianpu_mingcheng,
+ 'zhuangtai': shop.zhuangtai,
+ 'yonghu_id': shop.pingzheng.yonghu if shop.pingzheng else '',
+ 'lianxi_dianhua': shop.lianxi_dianhua or '',
+ 'weixinhao': shop.weixinhao or '',
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': shop_list,
+ 'total': paginator.count
+ }
+ })
+
+
+# ==================== 3. 店铺商品类型映射列表(含筛选、待审核计数) ====================
+class ShopProductTypeMappingView(APIView):
+ """
+ 返回店铺商品类型映射(ShangpinLeixingDianpu)列表。
+ 支持:店铺ID、公共类型ID、上架/封禁/审核状态筛选,关键字搜索(介绍/ID/店铺名/公共类型名)。
+ 每次请求同时返回“待审核总数”(shenhe_zhuangtai=False 的记录数)。
+ 前端请求路径:/houtai/htdphqyssplx
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ username = request.data.get('username')
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return Response({'code': 403, 'msg': '身份验证失败'})
+ if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
+ return Response({'code': 403, 'msg': '无权访问商品类型映射'})
+
+ # ---------- 安全解析参数 ----------
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 10))
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '分页参数格式错误'})
+
+ keyword = str(request.data.get('keyword', '')).strip()
+ shop_id = request.data.get('shop_id')
+ public_type_id = request.data.get('public_type_id')
+
+ # 布尔型筛选字段处理:None 表示不筛选,否则转为布尔值
+ def parse_bool(val):
+ if val is None:
+ return None
+ return bool(val)
+
+ shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
+ fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
+ shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
+
+ # ---------- 查询(连表店铺和公共类型) ----------
+ mappings = ShangpinLeixingDianpu.objects.select_related('dianpu', 'gonggong_leixing').all()
+
+ # 筛选条件:均为参数化,完全防注入
+ if shop_id is not None:
+ mappings = mappings.filter(dianpu_id=int(shop_id))
+ if public_type_id is not None:
+ mappings = mappings.filter(gonggong_leixing_id=int(public_type_id))
+ if shangjia is not None:
+ mappings = mappings.filter(shangjia_zhuangtai=shangjia)
+ if fengjin is not None:
+ mappings = mappings.filter(fengjin_zhuangtai=fengjin)
+ if shenhe is not None:
+ mappings = mappings.filter(shenhe_zhuangtai=shenhe)
+
+ # 关键字搜索(介绍、ID、店铺名、公共类型名)
+ if keyword:
+ mappings = mappings.filter(
+ models.Q(jieshao__icontains=keyword) |
+ models.Q(id__icontains=keyword) |
+ models.Q(dianpu__dianpu_mingcheng__icontains=keyword) |
+ models.Q(gonggong_leixing__jieshao__icontains=keyword)
+ )
+
+ # ---------- 待审核总数(不受分页影响) ----------
+ pending_total = mappings.filter(shenhe_zhuangtai=False).count()
+
+ # ---------- 分页 ----------
+ paginator = Paginator(mappings, page_size)
+ page_obj = paginator.get_page(page)
+
+ mapping_list = []
+ for m in page_obj:
+ mapping_list.append({
+ 'id': m.id,
+ 'dianpu_id': m.dianpu_id,
+ 'dianpu_name': m.dianpu.dianpu_mingcheng if m.dianpu else '',
+ 'gonggong_leixing_id': m.gonggong_leixing_id,
+ 'gonggong_leixing_name': m.gonggong_leixing.jieshao if m.gonggong_leixing else '',
+ 'jieshao': m.jieshao,
+ 'tupian_url': m.tupian_url or '',
+ 'paixu': m.paixu,
+ 'shangjia_zhuangtai': m.shangjia_zhuangtai,
+ 'fengjin_zhuangtai': m.fengjin_zhuangtai,
+ 'shenhe_zhuangtai': m.shenhe_zhuangtai,
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': mapping_list,
+ 'total': paginator.count,
+ 'pending_total': pending_total
+ }
+ })
+
+
+# ==================== 统一修改接口(已修正事务) ====================
+class ShopProductModifyView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ username = request.data.get('username')
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return Response({'code': 403, 'msg': '身份验证失败'})
+
+ user_perms = set(permissions)
+ action = request.data.get('action')
+
+ if action == 'update_audit_mode':
+ return self._update_audit_mode(request, user_perms)
+ elif action == 'update_mapping_status':
+ return self._update_mapping_status(request, user_perms)
+ else:
+ return Response({'code': 400, 'msg': '未知操作'})
+
+ @transaction.atomic # ✅ 事务修复点
+ def _update_audit_mode(self, request, user_perms):
+ if '1199ab' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限修改审核模式'})
+
+ kaiqi = request.data.get('kaiqi_shenhe')
+ if kaiqi is None:
+ return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'})
+
+ try:
+ config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1)
+ config.kaiqi_shenhe = bool(kaiqi)
+ config.save(update_fields=['kaiqi_shenhe'])
+ return Response({'code': 0, 'msg': '审核模式已更新'})
+ except Exception as e:
+ logger.exception("更新审核模式失败")
+ return Response({'code': 500, 'msg': '服务器错误'})
+
+ @transaction.atomic
+ def _update_mapping_status(self, request, user_perms):
+ mapping_id = request.data.get('mapping_id')
+ if not mapping_id:
+ return Response({'code': 400, 'msg': '缺少 mapping_id'})
+
+ try:
+ mapping = ShangpinLeixingDianpu.objects.select_for_update().get(id=int(mapping_id))
+ except (ShangpinLeixingDianpu.DoesNotExist, ValueError):
+ return Response({'code': 404, 'msg': '映射不存在'})
+
+ fengjin = request.data.get('fengjin_zhuangtai')
+ shangjia = request.data.get('shangjia_zhuangtai')
+ shenhe = request.data.get('shenhe_zhuangtai')
+
+ # 权限判断
+ if fengjin is not None:
+ if fengjin:
+ if '1199abc' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限封禁'})
+ else:
+ if '1199abd' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限解封'})
+ mapping.fengjin_zhuangtai = bool(fengjin)
+
+ if shangjia is not None:
+ if not shangjia:
+ if '1199abc' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限下架'})
+ else:
+ if '1199abd' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限上架'})
+ mapping.shangjia_zhuangtai = bool(shangjia)
+
+ if shenhe is not None:
+ if shenhe:
+ if '1199abd' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限审核通过'})
+ mapping.shenhe_zhuangtai = bool(shenhe)
+
+ mapping.save()
+ return Response({'code': 0, 'msg': '修改成功'})
+
+
+
+
+
+
+# 商品管理相关权限组(拥有任意一个即可查看列表)
+PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd', '1199abe']
+
+
+class ProductListView(APIView):
+ """
+ 商品列表接口,支持多条件筛选与分页。
+ 请求路径: /houtai/hqhtdpsplx
+ 权限要求: 1199ab / 1199abc / 1199abd / 1199abe 任一即可
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # ----- 身份与权限验证 -----
+ username = request.data.get('username')
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return Response({'code': 403, 'msg': '身份验证失败'})
+
+ if not set(permissions).intersection(PRODUCT_PERMS):
+ return Response({'code': 403, 'msg': '无商品管理权限'})
+
+ # ----- 安全解析分页与筛选参数 -----
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 15))
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '分页参数格式错误'})
+
+ keyword = str(request.data.get('keyword', '')).strip()
+ shop_id = request.data.get('shop_id') # 店铺ID (可选)
+ shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型映射ID (可选)
+ public_type_id = request.data.get('public_type_id') # 公共商品类型ID (可选)
+
+ # 布尔型状态筛选
+ def parse_bool(val):
+ if val is None:
+ return None
+ return bool(val)
+
+ shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
+ shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
+ fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
+
+ # ----- 构建查询(预加载店铺和店铺商品类型) -----
+ products = Shangpin.objects.select_related('dianpu', 'dianpu_leixing').all()
+
+ # 按店铺筛选
+ if shop_id is not None:
+ try:
+ products = products.filter(dianpu_id=int(shop_id))
+ except (ValueError, TypeError):
+ return Response({'code': 400, 'msg': 'shop_id 格式错误'})
+
+ # 按店铺商品类型映射筛选
+ if shop_product_type_id is not None:
+ try:
+ products = products.filter(dianpu_leixing_id=int(shop_product_type_id))
+ except (ValueError, TypeError):
+ return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'})
+
+ # 按公共商品类型筛选(leixing_id 存储的是公共类型的ID)
+ if public_type_id is not None:
+ try:
+ products = products.filter(leixing_id=int(public_type_id))
+ except (ValueError, TypeError):
+ return Response({'code': 400, 'msg': 'public_type_id 格式错误'})
+
+ # 平台审核状态筛选 (BooleanField)
+ if shenhe is not None:
+ products = products.filter(shenhe_zhuangtai=shenhe)
+
+ # 上架状态筛选
+ if shangjia is not None:
+ products = products.filter(shangjia_zhuangtai=shangjia)
+
+ # 封禁状态筛选
+ if fengjin is not None:
+ products = products.filter(fengjin_zhuangtai=fengjin)
+
+ # 关键词搜索:商品ID或商品标题(搜索具有模糊性,使用icontains)
+ if keyword:
+ # 尝试纯数字可能是商品ID,使用精确匹配与标题模糊搜索
+ products = products.filter(
+ models.Q(id__icontains=keyword) |
+ models.Q(biaoqian__icontains=keyword)
+ )
+
+ # ----- 获取待审核总数(平台审核未通过的数量) -----
+ pending_total = products.filter(shenhe_zhuangtai=False).count()
+
+ # ----- 分页 -----
+ paginator = Paginator(products, page_size)
+ page_obj = paginator.get_page(page)
+
+ # ----- 构造返回数据(批量获取公共类型名称,避免N+1) -----
+ # 提取所有公共商品类型ID
+ type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None}
+ public_type_map = {}
+ if type_ids:
+ public_types = ShangpinLeixing.objects.filter(id__in=type_ids).values('id', 'jieshao')
+ public_type_map = {t['id']: t['jieshao'] for t in public_types}
+
+ product_list = []
+ for p in page_obj:
+ product_list.append({
+ 'id': p.id,
+ 'biaoqian': p.biaoqian or '',
+ 'jiage': p.jiage,
+ 'kucun': p.kucun,
+ 'leixing_id': p.leixing_id,
+ 'public_type_name': public_type_map.get(p.leixing_id, ''),
+ 'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
+ 'duiwai_xiaoliang': p.duiwai_xiaoliang,
+ 'jieshao': p.jieshao or '',
+ 'xiadan_xuzhi': p.xiadan_xuzhi or '',
+ 'guize_tupian': p.guize_tupian or '',
+ 'yaoqiuleixing': p.yaoqiuleixing,
+ 'huiyuan_id': p.huiyuan_id or '',
+ 'yongjin': p.yongjin,
+ 'kaioi_ewai_dashou_fencheng': p.kaioi_ewai_dashou_fencheng,
+ 'ewai_dashou_fencheng': p.ewai_dashou_fencheng,
+ 'create_time': p.create_time.isoformat() if p.create_time else '',
+ 'update_time': p.update_time.isoformat() if p.update_time else '',
+ 'paixu': p.paixu,
+ 'dianpu_id': p.dianpu_id,
+ 'dianpu_name': p.dianpu.dianpu_mingcheng if p.dianpu else '',
+ 'dianpu_leixing_id': p.dianpu_leixing_id,
+ 'dianpu_leixing_name': p.dianpu_leixing.jieshao if p.dianpu_leixing else '',
+ 'shangjia_zhuangtai': p.shangjia_zhuangtai,
+ 'fengjin_zhuangtai': p.fengjin_zhuangtai,
+ 'shenhe_zhuangtai': p.shenhe_zhuangtai,
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': product_list,
+ 'total': paginator.count,
+ 'pending_total': pending_total, # 待审核商品总数
+ }
+ })
+
+
+class ProductModifyView(APIView):
+ """
+ 商品修改接口,根据修改内容细粒度鉴权。
+ 请求路径: /houtai/htxgdpspsj
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ username = request.data.get('username')
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return Response({'code': 403, 'msg': '身份验证失败'})
+
+ user_perms = set(permissions)
+ action = request.data.get('action', '')
+
+ if action != 'update_product':
+ return Response({'code': 400, 'msg': '未知操作'})
+
+ return self._update_product(request, user_perms)
+
+ @transaction.atomic
+ def _update_product(self, request, user_perms):
+ """执行商品修改,严格校验权限"""
+ product_id = request.data.get('product_id')
+ if not product_id:
+ return Response({'code': 400, 'msg': '缺少商品ID'})
+
+ try:
+ # 行级锁防止并发修改
+ product = Shangpin.objects.select_for_update().get(id=int(product_id))
+ except (Shangpin.DoesNotExist, ValueError):
+ return Response({'code': 404, 'msg': '商品不存在'})
+
+ # 需要修改的字段
+ fields_to_update = {}
+ # 将要修改的状态标志用于后续权限判断
+ status_changes = {
+ 'shangjia': request.data.get('shangjia_zhuangtai'),
+ 'fengjin': request.data.get('fengjin_zhuangtai'),
+ 'shenhe': request.data.get('shenhe_zhuangtai'),
+ }
+
+ # ---------- 权限分类检查 ----------
+ # 1. 封禁/下架操作 (消极操作) -> 1199abc
+ if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']):
+ if '1199abc' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限执行下架/封禁/驳回审核操作(1199abc)'})
+ if status_changes.get('fengjin') is not None and status_changes['fengjin'] == True:
+ if '1199abc' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限执行封禁操作(1199abc)'})
+
+ # 2. 解封/上架/过审 (积极操作) -> 1199abd
+ if any(status_changes.get(k) is not None and status_changes[k] == True for k in ['shangjia', 'shenhe']):
+ if '1199abd' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限执行上架/解封/审核通过操作(1199abd)'})
+ if status_changes.get('fengjin') is not None and status_changes['fengjin'] == False:
+ if '1199abd' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'})
+
+ # 3. 修改其他数据(非状态字段)-> 1199abe
+ # 除了上架、封禁、审核之外的字段都属于“其他数据”
+ other_fields = [k for k in request.data.keys() if k not in ['action', 'product_id', 'username',
+ 'shangjia_zhuangtai', 'fengjin_zhuangtai',
+ 'shenhe_zhuangtai']]
+ if other_fields and '1199abe' not in user_perms:
+ return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'})
+
+ # ---------- 应用修改 ----------
+ # 允许修改的字段白名单(防止注入)
+ allowed_fields = [
+ 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'duiwai_xiaoliang',
+ 'jieshao', 'xiadan_xuzhi', 'yaoqiuleixing', 'yongjin',
+ 'kaioi_ewai_dashou_fencheng', 'ewai_dashou_fencheng',
+ 'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai'
+ ]
+
+ for field in allowed_fields:
+ value = request.data.get(field)
+ if value is not None:
+ # 简单类型校验(根据字段类型)
+ if field in ['jiage', 'kucun', 'duiwai_xiaoliang', 'yongjin', 'ewai_dashou_fencheng', 'paixu']:
+ try:
+ value = float(value) if 'jiage' in field or 'yongjin' in field or 'ewai' in field else int(value)
+ except (ValueError, TypeError):
+ return Response({'code': 400, 'msg': f'字段 {field} 值格式错误'})
+ setattr(product, field, value)
+
+ product.save(update_fields=allowed_fields)
+ return Response({'code': 0, 'msg': '商品修改成功'})
+
+
+
+
+# 与前端约定的聊天权限码
+CHAT_PERM_CODES = ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4']
+
+class KefuChatPermissionsView(APIView):
+ """
+ 获取客服聊天权限及GoEasy配置
+ POST /houtai/kfhqltqx
+ Body: { "phone": "客服手机号" }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 从前端获取传递的账号,用于防越权校验
+ username_from_frontend = request.data.get('phone', None)
+
+ # 2. 调用公共验证方法。
+ # 返回 (kefu_obj, permissions) 或 (None, Response)
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+
+ # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值(错误Response)
+ if kefu_obj is None:
+ return permissions
+
+ # ---------- 验证通过,permissions 已经是权限码列表 ----------
+ # 4. 检查是否拥有至少一个聊天权限
+ has_chat_perm = any(p in CHAT_PERM_CODES for p in permissions)
+
+ # 5. 若有聊天权限,查询我方俱乐部的 chat_api_key
+ goeasy_appkey = ''
+ if has_chat_perm:
+ config = ClubConfig.objects.filter(is_self=1).first()
+ if config:
+ goeasy_appkey = config.chat_api_key or ''
+
+ # 6. 返回数据
+ return Response({
+ 'code': 0,
+ 'msg': 'ok',
+ 'data': {
+ 'permissions': permissions,
+ 'goeasy_appkey': goeasy_appkey
+ }
+ })
+
+
+
+
+
+class FineApplyView(APIView):
+ """
+ 客服申请罚款接口(金额罚款,非积分)
+ 路径:POST /houtai/kffkdssq
+ 参数:
+ username: 客服手机号
+ dingdan_id: 订单ID
+ chufaliyou: 罚款原因
+ fakuanjine: 罚款金额(元)
+ yingxiang_qiangdan: 1影响抢单 / 0不影响抢单
+ shenqingren_shenfen: 操作人身份 (1客服/2售后/3管理员等)
+ 权限:JWT + 权限码99933abs
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 提取参数
+ username = request.data.get('username', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ chufaliyou = request.data.get('chufaliyou', '').strip()
+ fakuanjine = request.data.get('fakuanjine', 0)
+ yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1)
+ shenqingren_shenfen = request.data.get('shenqingren_shenfen', 2)
+
+ if not all([username, dingdan_id, chufaliyou, fakuanjine]):
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ try:
+ # 2. 权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if '99933abs' not in permissions:
+ return Response({'code': 403, 'msg': '您无罚款权限'})
+
+ # 3. 获取订单(无需 select_for_update,因不修改订单)
+ try:
+ order = Dingdan.objects.get(dingdan_id=dingdan_id)
+ except Dingdan.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'})
+
+ dashou_id = order.jiedan_dashou_id
+ if not dashou_id:
+ return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'})
+
+ # 4. 重复罚单检查
+ if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists():
+ return Response({'code': 400, 'msg': '该打手已被罚款过'})
+
+ # 5. 跨平台相关字段
+ partner_order_id = order.partner_order_id or ''
+ partner_club_id = order.partner_club_id or ''
+ dispatch_type = order.dispatch_type or 0
+
+ # 本地创建罚单的可复用函数
+ def create_local_fadan():
+ Fadan.objects.create(
+ beichufa_id=dashou_id,
+ shenqing_chufa=username,
+ shenfen=1, # 1 代表打手
+ chufaliyou=chufaliyou,
+ fakuanjine=fakuanjine,
+ guanliandingdan_id=dingdan_id,
+ zhuangtai=1, # 待缴纳
+ yingxiang_qiangdan=yingxiang_qiangdan,
+ shenqingren_shenfen=shenqingren_shenfen,
+ partner_club_id='',
+ )
+
+ # 6. 判断:没有对方订单ID,或对方派单 → 本地处罚
+ if not partner_order_id or dispatch_type == 2:
+ with transaction.atomic():
+ create_local_fadan()
+ logger.info(f"本地罚款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}")
+ return Response({'code': 0, 'msg': '罚款已生成'})
+
+ # 7. 我方派单且有对方订单ID → 通知对方平台
+ # 获取我方俱乐部ID
+ try:
+ my_club = ClubConfig.objects.filter(is_self=1).first()
+ if not my_club:
+ return Response({'code': 500, 'msg': '系统配置错误'})
+ self_club_id = my_club.club_id
+ club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
+ partner_domain = club_rel.partner_domain
+ except ObjectDoesNotExist:
+ return Response({'code': 500, 'msg': '对方平台信息未配置'})
+
+ url = partner_domain.rstrip('/') + '/houtai/kffkdssq_notify'
+ payload = {
+ 'order_id': partner_order_id,
+ 'club_id': self_club_id,
+ 'chufaliyou': chufaliyou,
+ 'fakuanjine': fakuanjine,
+ 'yingxiang_qiangdan': yingxiang_qiangdan,
+ 'shenqingren_shenfen': shenqingren_shenfen,
+ 'operator': username,
+ }
+ timeout = 3
+ retries = 2
+ success = False
+ for attempt in range(retries):
+ try:
+ resp = requests.post(url, json=payload, timeout=timeout)
+ if resp.status_code == 200 and resp.json().get('code') == 0:
+ success = True
+ break
+ except Exception:
+ continue
+
+ if not success:
+ return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'})
+
+ # 通知成功后,本地也记录一笔(标记为跨平台)
+ with transaction.atomic():
+ Fadan.objects.create(
+ beichufa_id=dashou_id,
+ shenqing_chufa=username,
+ shenfen=1,
+ chufaliyou=chufaliyou,
+ fakuanjine=fakuanjine,
+ guanliandingdan_id=dingdan_id,
+ zhuangtai=1,
+ yingxiang_qiangdan=yingxiang_qiangdan,
+ shenqingren_shenfen=shenqingren_shenfen,
+ partner_club_id=partner_club_id,
+ )
+ logger.info(f"跨平台罚款通知成功: {dingdan_id}")
+ return Response({'code': 0, 'msg': '罚款已提交至对方平台'})
+
+ except Exception as e:
+ logger.error(f"罚款申请异常: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+
+
+
+
+class PartnerFineNotifyView(APIView):
+ """
+ 接收对方平台罚款通知(无认证)
+ 路径:POST /houtai/kffkdssq_notify
+ 参数:
+ order_id: 对方平台的订单ID(需通过这个找到我方订单)
+ club_id: 对方俱乐部ID
+ chufaliyou: 罚款原因
+ fakuanjine: 罚款金额
+ yingxiang_qiangdan: 1影响抢单 / 0不影响
+ shenqingren_shenfen: 申请处罚人身份
+ operator: 操作人标识
+ """
+ authentication_classes = []
+ permission_classes = [AllowAny]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # token = request.data.get('token')
+ # if not token or not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN):
+ # return Response({'code': 403, 'msg': '无效的跨平台令牌'}, status=200)
+ try:
+ order_id = request.data.get('order_id', '').strip() # 对方平台订单ID
+ club_id = request.data.get('club_id', '').strip()
+ chufaliyou = request.data.get('chufaliyou', '').strip()
+ fakuanjine = request.data.get('fakuanjine', 0)
+ yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1)
+ shenqingren_shenfen = request.data.get('shenqingren_shenfen', 2)
+ operator = request.data.get('operator', '').strip()
+
+ if not all([order_id, chufaliyou]):
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 通过对方订单ID查找我方订单
+ try:
+ cross = CrossPlatformOrderData.objects.get(partner_order_id=order_id)
+ my_dingdan_id = cross.dingdan_id
+ order = Dingdan.objects.get(dingdan_id=my_dingdan_id)
+ dashou_id = order.jiedan_dashou_id
+ except CrossPlatformOrderData.DoesNotExist:
+ return Response({'code': 404, 'msg': '未找到关联订单'})
+ except Dingdan.DoesNotExist:
+ return Response({'code': 404, 'msg': '我方订单不存在'})
+
+ if not dashou_id:
+ return Response({'code': 400, 'msg': '订单无接单打手'})
+
+ # 重复检查
+ if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=my_dingdan_id).exists():
+ return Response({'code': 0, 'msg': '已存在罚单'})
+
+ # 创建罚单
+ Fadan.objects.create(
+ beichufa_id=dashou_id,
+ shenqing_chufa=operator,
+ shenfen=1,
+ chufaliyou=chufaliyou,
+ fakuanjine=fakuanjine,
+ guanliandingdan_id=my_dingdan_id,
+ zhuangtai=1,
+ yingxiang_qiangdan=yingxiang_qiangdan,
+ shenqingren_shenfen=shenqingren_shenfen,
+ partner_club_id=club_id,
+ )
+ logger.info(f"接收对方罚款成功: 订单 {my_dingdan_id}, 打手 {dashou_id}")
+ return Response({'code': 0, 'msg': 'success'})
+
+ except Exception as e:
+ logger.error(f"接收罚款通知异常: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+
+
+
+
+
+
+
+# 权限码到身份的映射
+PERMISSION_TO_SHENFEN = {
+ '66693a': 1, # 打手
+ '66693b': 3, # 商家
+ '66693c': 2, # 管事
+ '66694c': 4, # 组长
+}
+
+# 身份对应的用户扩展关系名
+SHENFEN_PROFILE_MAP = {
+ 1: 'dashou_profile',
+ 2: 'guanshi_profile',
+ 3: 'shop_profile',
+ 4: 'zuzhang_profile',
+}
+
+
+def check_fadan_permission(permissions, shenfen):
+ """检查是否有处理指定身份罚单的权限"""
+ perm_code = {1: '66693a', 2: '66693c', 3: '66693b', 4: '66694c'}.get(shenfen)
+ return perm_code in permissions
+
+
+class FaKuanTongJiView(APIView):
+ """
+ 罚款统计接口
+ POST /houtai/htglyhqcfsltj
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ try:
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ # 至少有一个罚款管理权限
+ if not any(p in permissions for p in PERMISSION_TO_SHENFEN.keys()):
+ return Response({'code': 403, 'msg': '无权限查看罚款统计'})
+
+ stats = Fadan.objects.aggregate(
+ total=Count('id'),
+ daijiaona=Count('id', filter=Q(zhuangtai=1)),
+ shensuzhong=Count('id', filter=Q(zhuangtai=3)),
+ yijiaona=Count('id', filter=Q(zhuangtai=2)),
+ yibohui=Count('id', filter=Q(zhuangtai=4)),
+ )
+ data = {k: v or 0 for k, v in stats.items()}
+ return Response({'code': 0, 'msg': '成功', 'data': data})
+ except Exception as e:
+ logger.error(f"罚款统计异常: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '系统繁忙'})
+
+
+class FaKuanLieBiaoView(APIView):
+ """
+ 罚款列表接口
+ POST /houtai/hthqfklb
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ try:
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+ if not any(p in permissions for p in PERMISSION_TO_SHENFEN.keys()):
+ return Response({'code': 403, 'msg': '无权限查看罚款列表'})
+
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 20))
+ if page_size > 100:
+ page_size = 100
+
+ qs = Fadan.objects.all()
+
+ # 状态筛选
+ zhuangtai = request.data.get('zhuangtai')
+ if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]:
+ qs = qs.filter(zhuangtai=int(zhuangtai))
+
+ # 被处罚人ID
+ beichufa_id = request.data.get('beichufa_id', '').strip()
+ if beichufa_id:
+ qs = qs.filter(beichufa_id=beichufa_id)
+
+ # 被处罚人身份
+ beichufa_shenfen = request.data.get('beichufa_shenfen')
+ if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]:
+ qs = qs.filter(shenfen=int(beichufa_shenfen))
+
+ # 模糊搜索(订单ID、被罚人ID、申请人ID)
+ sousuo = request.data.get('sousuo', '').strip()
+ if sousuo:
+ qs = qs.filter(
+ Q(guanliandingdan_id__icontains=sousuo) |
+ Q(beichufa_id__icontains=sousuo) |
+ Q(shenqing_chufa__icontains=sousuo)
+ )
+
+ qs = qs.order_by('-create_time')
+ paginator = Paginator(qs, page_size)
+ try:
+ page_obj = paginator.page(page)
+ except Exception:
+ return Response({
+ 'code': 0, 'msg': '成功',
+ 'data': {'list': [], 'total': 0, 'page': page, 'page_size': page_size}
+ })
+
+ records = list(page_obj)
+ fadan_ids = [r.id for r in records]
+
+ # 批量获取图片并分组
+ zhengju_map = {}
+ shensu_map = {}
+ if fadan_ids:
+ tupians = FadanShensuTupian.objects.filter(
+ fadan_id__in=fadan_ids
+ ).values('fadan_id', 'tupian_url', 'yongtu')
+ for t in tupians:
+ fid = t['fadan_id']
+ url = t['tupian_url']
+ if t['yongtu'] == 1:
+ zhengju_map.setdefault(fid, []).append(url)
+ else:
+ shensu_map.setdefault(fid, []).append(url)
+
+ # 组装列表数据
+ results = []
+ for r in records:
+ results.append({
+ 'id': r.id,
+ 'beichufa_id': r.beichufa_id,
+ 'shenfen': r.shenfen,
+ 'shenfen_text': dict(Fadan._meta.get_field('shenfen').choices).get(r.shenfen, ''),
+ 'shenqing_chufa': r.shenqing_chufa or '',
+ 'shenqingren_shenfen': r.shenqingren_shenfen,
+ 'chufaliyou': r.chufaliyou or '',
+ 'fakuanjine': str(r.fakuanjine),
+ 'guanliandingdan_id': r.guanliandingdan_id or '',
+ 'zhuangtai': r.zhuangtai,
+ 'yingxiang_qiangdan': r.yingxiang_qiangdan,
+ 'shensuliyou': r.shensuliyou or '',
+ 'bohuiliyou': r.bohuiliyou or '',
+ 'chulizhe': r.chulizhe or '',
+ 'chulizhe_shenfen': r.chulizhe_shenfen,
+ 'zhengju_tupian': zhengju_map.get(r.id, []),
+ 'shensu_tupian': shensu_map.get(r.id, []),
+ 'create_time': r.create_time.strftime('%Y-%m-%d %H:%M:%S') if r.create_time else '',
+ 'update_time': r.update_time.strftime('%Y-%m-%d %H:%M:%S') if r.update_time else '',
+ })
+
+ return Response({
+ 'code': 0, 'msg': '成功',
+ 'data': {
+ 'list': results,
+ 'total': paginator.count,
+ 'page': page,
+ 'page_size': page_size,
+ }
+ })
+ except Exception as e:
+ logger.error(f"罚款列表异常: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '系统繁忙'})
+
+
+class FaKuanChuLiView(APIView):
+ """
+ 处理罚款申诉(同意/拒绝)
+ POST /houtai/glyclfk
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [MultiPartParser, FormParser, JSONParser]
+
+ def post(self, request):
+ try:
+ username = request.data.get('username', '').strip()
+ fadan_id = request.data.get('fadan_id')
+ action = request.data.get('action', '').strip()
+ chuli_liyou = request.data.get('chuli_liyou', '').strip()
+
+ if not all([username, fadan_id, action]):
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ with transaction.atomic():
+ try:
+ fadan = Fadan.objects.select_for_update().get(id=fadan_id)
+ except Fadan.DoesNotExist:
+ return Response({'code': 404, 'msg': '罚单不存在'})
+
+ if fadan.zhuangtai != 3:
+ return Response({'code': 400, 'msg': '当前状态不可处理'})
+
+ if not check_fadan_permission(permissions, fadan.shenfen):
+ return Response({'code': 403, 'msg': '无权限处理该类罚单'})
+
+ # 处理上传的图片
+ uploaded_urls = []
+ files = request.FILES.getlist('tupian') or []
+ if not files:
+ single = request.FILES.get('tupian')
+ if single:
+ files = [single]
+ for file in files:
+ valid, msg = validate_image(file)
+ if not valid:
+ return Response({'code': 400, 'msg': msg})
+ ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
+ file_name = f"chufa_{fadan_id}_{int(time.time() * 1000)}.{ext}"
+ file_path = f"chufatupian/kechufatupian/{file_name}"
+ url = upload_to_oss(file, file_path)
+ if not url:
+ return Response({'code': 500, 'msg': '图片上传失败'})
+ uploaded_urls.append(file_path)
+
+ # 更新状态
+ if action == 'agree': # 同意申诉 → 驳回处罚
+ fadan.zhuangtai = 4
+ fadan.bohuiliyou = chuli_liyou
+ elif action == 'reject': # 拒绝申诉 → 回到待缴纳
+ fadan.zhuangtai = 1
+ fadan.bohuiliyou = chuli_liyou
+ else:
+ return Response({'code': 400, 'msg': '无效的操作类型'})
+
+ # 记录处理者(使用 request.user 的 phone)
+ fadan.chulizhe = request.user.phone
+ fadan.chulizhe_shenfen = 2 # 这里假设处理者是售后身份,可根据实际业务获取
+ fadan.save()
+
+ # 保存图片(证据图片,用途1)
+ for relative_url in uploaded_urls:
+ FadanShensuTupian.objects.create(
+ fadan_id=fadan_id,
+ beichufa_id=fadan.beichufa_id,
+ tupian_url=relative_url,
+ yongtu=1
+ )
+
+ logger.info(f"罚款处理成功: {fadan_id}, 操作: {action}")
+ return Response({'code': 0, 'msg': '处理成功'})
+
+ except Exception as e:
+ logger.error(f"罚款处理异常: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '系统繁忙'})
+
+
+class FaKuanChuangJianView(APIView):
+ """
+ 管理员创建罚款
+ POST /houtai/htfksc
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [MultiPartParser, FormParser, JSONParser]
+
+ def post(self, request):
+ try:
+ username = request.data.get('username', '').strip()
+ beichufa_id = request.data.get('beichufa_id', '').strip()
+ shenfen = request.data.get('shenfen')
+ chufaliyou = request.data.get('chufaliyou', '').strip()
+ fakuanjine = request.data.get('fakuanjine')
+ yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1)
+
+ if not all([username, beichufa_id, shenfen, chufaliyou, fakuanjine]):
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ try:
+ shenfen = int(shenfen)
+ fakuanjine = float(fakuanjine)
+ except (ValueError, TypeError):
+ return Response({'code': 400, 'msg': '参数格式错误'})
+
+ if shenfen not in [1, 2, 3, 4]:
+ return Response({'code': 400, 'msg': '无效的被罚人身份'})
+
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ if not check_fadan_permission(permissions, shenfen):
+ return Response({'code': 403, 'msg': '无权限创建该类罚单'})
+
+ # 验证用户是否存在对应的扩展表
+ try:
+ user = UserMain.objects.get(yonghuid=beichufa_id)
+ profile_attr = SHENFEN_PROFILE_MAP.get(shenfen)
+ if not profile_attr or not hasattr(user, profile_attr):
+ return Response({'code': 400, 'msg': '该用户不是所选身份'})
+ except UserMain.DoesNotExist:
+ return Response({'code': 404, 'msg': '用户不存在'})
+
+ # 处理图片上传
+ uploaded_urls = []
+ files = request.FILES.getlist('tupian') or []
+ if not files:
+ single = request.FILES.get('tupian')
+ if single:
+ files = [single]
+ for file in files:
+ valid, msg = validate_image(file)
+ if not valid:
+ return Response({'code': 400, 'msg': msg})
+ ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
+ file_name = f"chufa_{int(time.time() * 1000)}.{ext}"
+ file_path = f"chufatupian/kechufatupian/{file_name}"
+ url = upload_to_oss(file, file_path)
+ if not url:
+ return Response({'code': 500, 'msg': '图片上传失败'})
+ uploaded_urls.append(file_path)
+
+ with transaction.atomic():
+ fadan = Fadan.objects.create(
+ beichufa_id=beichufa_id,
+ shenfen=shenfen,
+ shenqing_chufa=request.user.phone, # 申请人(客服)手机号
+ shenqingren_shenfen=2, # 售后身份
+ chufaliyou=chufaliyou,
+ fakuanjine=fakuanjine,
+ yingxiang_qiangdan=yingxiang_qiangdan,
+ zhuangtai=1, # 待缴纳
+ )
+ for relative_url in uploaded_urls:
+ FadanShensuTupian.objects.create(
+ fadan=fadan,
+ beichufa_id=beichufa_id,
+ tupian_url=relative_url,
+ yongtu=1
+ )
+
+ logger.info(f"罚款创建成功: {fadan.id}")
+ return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': fadan.id}})
+
+ except Exception as e:
+ logger.error(f"创建罚款异常: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '系统繁忙'})
+
+
+
+
+
+
+
+
+
+class HqbkxxView(APIView):
+ """
+ 获取板块配置所需全部数据
+ POST /houtai/hqbkxx
+ Body: { "phone": "管理员手机号" }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 从前端获取传递的账号,用于防越权校验
+ username_from_frontend = request.data.get('phone', None)
+
+ # 2. 调用公共验证方法
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+
+ # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值
+ if kefu_obj is None:
+ return permissions
+
+ # ---------- 验证通过 ----------
+ # 4. 检查是否有板块管理权限
+ if 'bankuai' not in permissions:
+ return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
+
+ # 5. 查询所有板块
+ bankuai_list = []
+ for bk in Bankuai.objects.all():
+ bankuai_list.append({
+ 'bankuai_id': bk.bankuai_id,
+ 'mingcheng': bk.mingcheng,
+ 'create_time': bk.create_time.strftime('%Y-%m-%d %H:%M:%S') if bk.create_time else ''
+ })
+
+ # 6. 查询所有商品类型
+ shangpin_leixing_list = []
+ for sp in ShangpinLeixing.objects.all():
+ shangpin_leixing_list.append({
+ 'id': sp.id,
+ 'jieshao': sp.jieshao,
+ 'tupian_url': sp.tupian_url or '',
+ 'bankuai_id': sp.bankuai_id
+ })
+
+ # 7. 查询所有会员
+ huiyuan_list = []
+ for hy in Huiyuan.objects.all():
+ huiyuan_list.append({
+ 'huiyuan_id': hy.huiyuan_id,
+ 'jieshao': hy.jieshao,
+ 'bankuai_id': hy.bankuai_id
+ })
+
+ return Response({
+ 'code': 0,
+ 'msg': 'ok',
+ 'data': {
+ 'bankuai': bankuai_list,
+ 'shangpin_leixing': shangpin_leixing_list,
+ 'huiyuan': huiyuan_list
+ }
+ })
+
+
+class BkxgView(APIView):
+ """
+ 板块增删改统一接口
+ POST /houtai/bkxg
+ Body: {
+ "phone": "管理员手机号",
+ "action": "create | update | delete | add_items | remove_items",
+ "bankuai_id": int,
+ "bankuai_name": str,
+ "item_type": "shangpin_leixing | huiyuan",
+ "item_ids": [1,2,3]
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 从前端获取传递的账号
+ username_from_frontend = request.data.get('phone', None)
+
+ # 2. 调用公共验证方法
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+
+ # 3. 验证失败直接返回
+ if kefu_obj is None:
+ return permissions
+
+ # 4. 检查板块管理权限
+ if 'bankuai' not in permissions:
+ return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
+
+ # 5. 获取操作类型
+ action = request.data.get('action')
+ if not action:
+ return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
+
+ # ==================== 创建板块 ====================
+ if action == 'create':
+ bankuai_name = request.data.get('bankuai_name', '').strip()
+ if not bankuai_name:
+ return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
+ if Bankuai.objects.filter(mingcheng=bankuai_name).exists():
+ return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
+
+ shangpin_ids = request.data.get('shangpin_ids', [])
+ huiyuan_ids = request.data.get('huiyuan_ids', [])
+
+ with transaction.atomic():
+ new_bk = Bankuai.objects.create(mingcheng=bankuai_name)
+ if shangpin_ids:
+ ShangpinLeixing.objects.filter(
+ id__in=shangpin_ids,
+ bankuai__isnull=True
+ ).update(bankuai=new_bk)
+ if huiyuan_ids:
+ Huiyuan.objects.filter(
+ huiyuan_id__in=huiyuan_ids,
+ bankuai__isnull=True
+ ).update(bankuai=new_bk)
+
+ return Response({
+ 'code': 0,
+ 'msg': '创建成功',
+ 'data': {'bankuai_id': new_bk.bankuai_id}
+ })
+
+ # ==================== 以下操作需要 bankuai_id ====================
+ bankuai_id = request.data.get('bankuai_id')
+ if not bankuai_id:
+ return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400)
+
+ try:
+ bk = Bankuai.objects.get(bankuai_id=bankuai_id)
+ except Bankuai.DoesNotExist:
+ return Response({'code': 1, 'msg': '板块不存在'}, status=404)
+
+ # ==================== 修改板块名称 ====================
+ if action == 'update':
+ new_name = request.data.get('bankuai_name', '').strip()
+ if not new_name:
+ return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
+ if Bankuai.objects.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists():
+ return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
+ bk.mingcheng = new_name
+ bk.save()
+ return Response({'code': 0, 'msg': '修改成功'})
+
+ # ==================== 删除板块 ====================
+ elif action == 'delete':
+ with transaction.atomic():
+ ShangpinLeixing.objects.filter(bankuai=bk).update(bankuai=None)
+ Huiyuan.objects.filter(bankuai=bk).update(bankuai=None)
+ bk.delete()
+ return Response({'code': 0, 'msg': '删除成功'})
+
+ # ==================== 添加绑定 ====================
+ elif action == 'add_items':
+ item_type = request.data.get('item_type')
+ item_ids = request.data.get('item_ids', [])
+ if not item_type or not item_ids:
+ return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
+
+ if item_type == 'shangpin_leixing':
+ ShangpinLeixing.objects.filter(
+ id__in=item_ids,
+ bankuai__isnull=True
+ ).update(bankuai=bk)
+ elif item_type == 'huiyuan':
+ Huiyuan.objects.filter(
+ huiyuan_id__in=item_ids,
+ bankuai__isnull=True
+ ).update(bankuai=bk)
+ else:
+ return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
+ return Response({'code': 0, 'msg': '添加成功'})
+
+ # ==================== 移除绑定 ====================
+ elif action == 'remove_items':
+ item_type = request.data.get('item_type')
+ item_ids = request.data.get('item_ids', [])
+ if not item_type or not item_ids:
+ return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
+
+ if item_type == 'shangpin_leixing':
+ ShangpinLeixing.objects.filter(
+ id__in=item_ids,
+ bankuai=bk
+ ).update(bankuai=None)
+ elif item_type == 'huiyuan':
+ Huiyuan.objects.filter(
+ huiyuan_id__in=item_ids,
+ bankuai=bk
+ ).update(bankuai=None)
+ else:
+ return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
+ return Response({'code': 0, 'msg': '移除成功'})
+
+ else:
+ return Response({'code': 1, 'msg': '未知操作'}, status=400)
+
+
+
+
+
+
+
+
+
+class CaiwuView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ username_from_frontend = request.data.get('phone', None)
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+
+ if kefu_obj is None:
+ return permissions
+
+ if 'caiwu' not in permissions:
+ return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
+
+ today = date.today()
+ today_start = datetime.combine(today, datetime.min.time())
+ today_end = today_start + timedelta(days=1)
+
+ # 今日收入
+ today_income = DailyIncomeStat.objects.filter(date=today).first()
+ today_income_amount = float(today_income.total_amount) if today_income else 0.00
+ today_income_count = today_income.total_count if today_income else 0
+
+ # 今日支出
+ today_payout = DailyPayoutStat.objects.filter(date=today).first()
+ today_payout_amount = float(today_payout.total_amount) if today_payout else 0.00
+ today_payout_count = today_payout.total_count if today_payout else 0
+
+ # 今日订单(已付款及之后的状态)
+ today_orders = Dingdan.objects.filter(
+ create_time__gte=today_start,
+ create_time__lt=today_end,
+ zhuangtai__in=[1,2,3,4,5,6,7,8]
+ )
+ today_order_count = today_orders.count()
+ today_order_amount = float(today_orders.aggregate(total=Sum('jine'))['total'] or 0.00)
+
+ # 今日成交(zhuangtai=3)
+ today_completed = Dingdan.objects.filter(
+ create_time__gte=today_start,
+ create_time__lt=today_end,
+ zhuangtai=3
+ )
+ today_completed_count = today_completed.count()
+ today_completed_amount = float(today_completed.aggregate(total=Sum('jine'))['total'] or 0.00)
+
+ # 今日退款(zhuangtai=5)
+ today_tuikuan = Dingdan.objects.filter(
+ create_time__gte=today_start,
+ create_time__lt=today_end,
+ zhuangtai=5
+ )
+ today_tuikuan_count = today_tuikuan.count()
+ today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('jine'))['total'] or 0.00)
+
+ # 今日充值(会员充值,leixing=1, zhuangtai=3)
+ today_chongzhi = Czjilu.objects.filter(
+ create_time__gte=today_start,
+ create_time__lt=today_end,
+ leixing=1,
+ zhuangtai=3
+ )
+ today_chongzhi_count = today_chongzhi.count()
+ today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00)
+
+ # 今日新增会员:从购买记录表统计今天创建的用户(去重)
+ new_huiyuan_user_ids = Huiyuangoumai.objects.filter(
+ create_time__gte=today_start,
+ create_time__lt=today_end
+ ).values_list('yonghu_id', flat=True).distinct()
+ today_new_huiyuan = len(new_huiyuan_user_ids)
+
+ # 新增会员充值总额(这些用户今天的会员充值金额)
+ today_new_huiyuan_amount = 0.0
+ if new_huiyuan_user_ids:
+ amount_sum = Czjilu.objects.filter(
+ create_time__gte=today_start,
+ create_time__lt=today_end,
+ yonghuid__in=new_huiyuan_user_ids,
+ leixing=1,
+ zhuangtai=3
+ ).aggregate(total=Sum('jine'))
+ today_new_huiyuan_amount = float(amount_sum['total'] or 0.0)
+
+ # 各角色可提现余额总和
+ all_guanshi_yue = float(UserGuanshi.objects.aggregate(total=Sum('yue'))['total'] or 0.00)
+ all_dashou_yue = float(UserDashou.objects.aggregate(total=Sum('yue'))['total'] or 0.00)
+ all_zuzhang_yue = float(UserZuzhang.objects.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00)
+ all_shangjia_yue = float(UserShangjia.objects.aggregate(total=Sum('yue'))['total'] or 0.00)
+
+ # 平台总收支及利润
+ total_income = float(DailyIncomeStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00)
+ total_payout = float(DailyPayoutStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00)
+ platform_profit = round(total_income - total_payout, 2)
+
+ # 近30天每日收支明细
+ daily_stats = []
+ income_qs = DailyIncomeStat.objects.order_by('-date')[:30]
+ payout_dict = {
+ str(p.date): float(p.total_amount)
+ for p in DailyPayoutStat.objects.filter(date__gte=today - timedelta(days=30))
+ }
+ for inc in income_qs:
+ d = str(inc.date)
+ daily_stats.append({
+ 'date': d,
+ 'income': float(inc.total_amount),
+ 'income_count': inc.total_count,
+ 'payout': payout_dict.get(d, 0.00),
+ 'profit': round(float(inc.total_amount) - payout_dict.get(d, 0.00), 2)
+ })
+
+ return Response({
+ 'code': 0,
+ 'msg': 'ok',
+ 'data': {
+ 'today_income': round(today_income_amount, 2),
+ 'today_income_count': today_income_count,
+ 'today_payout': round(today_payout_amount, 2),
+ 'today_payout_count': today_payout_count,
+ 'today_order_count': today_order_count,
+ 'today_order_amount': round(today_order_amount, 2),
+ 'today_completed_count': today_completed_count,
+ 'today_completed_amount': round(today_completed_amount, 2),
+ 'today_tuikuan_count': today_tuikuan_count,
+ 'today_tuikuan_amount': round(today_tuikuan_amount, 2),
+ 'today_chongzhi_count': today_chongzhi_count,
+ 'today_chongzhi_amount': round(today_chongzhi_amount, 2),
+ 'today_new_huiyuan': today_new_huiyuan,
+ 'today_new_huiyuan_amount': round(today_new_huiyuan_amount, 2),
+ 'all_guanshi_yue': round(all_guanshi_yue, 2),
+ 'all_dashou_yue': round(all_dashou_yue, 2),
+ 'all_zuzhang_yue': round(all_zuzhang_yue, 2),
+ 'all_shangjia_yue': round(all_shangjia_yue, 2),
+ 'total_income': round(total_income, 2),
+ 'total_payout': round(total_payout, 2),
+ 'platform_profit': platform_profit,
+ 'daily_stats': daily_stats
+ }
+ })
+
+
+
+
+
+class CwhybkhqView(APIView):
+ """
+ 获取所有板块及对应会员列表
+ POST /houtai/cwhybkhq
+ Body: { "phone": "管理员手机号" }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ username_from_frontend = request.data.get('phone', None)
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+ if kefu_obj is None:
+ return permissions
+
+ if 'huiyuancaiwu' not in permissions:
+ return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
+
+ bankuai_list = []
+ for bk in Bankuai.objects.all():
+ members = Huiyuan.objects.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id')
+ bankuai_list.append({
+ 'bankuai_id': bk.bankuai_id,
+ 'mingcheng': bk.mingcheng,
+ 'members': list(members)
+ })
+
+ return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list})
+ except Exception as e:
+ logger.exception("CwhybkhqView 接口错误")
+ return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
+
+
+class HybkjtsjView(APIView):
+ """
+ 会员详细统计数据
+ POST /houtai/hybkjtsj
+ Body: {
+ "phone": "管理员手机号",
+ "huiyuan_id": "会员ID",
+ "granularity": "day" | "month" | "year",
+ "year": 2026,
+ "month": 5,
+ "include_guanshi": true,
+ "include_zuzhang": true,
+ "summary_date": "2026-05-16"
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ username_from_frontend = request.data.get('phone', None)
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+ if kefu_obj is None:
+ return permissions
+
+ if 'huiyuancaiwu' not in permissions:
+ return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
+
+ huiyuan_id = request.data.get('huiyuan_id')
+ granularity = request.data.get('granularity', 'day')
+ year = request.data.get('year')
+ month = request.data.get('month')
+ include_guanshi = request.data.get('include_guanshi', False)
+ include_zuzhang = request.data.get('include_zuzhang', False)
+ summary_date = request.data.get('summary_date')
+
+ if not huiyuan_id:
+ return Response({'code': 1, 'msg': '缺少会员ID'}, status=400)
+
+ # 🔧 直接使用 date.today() 避免模块名冲突
+ now = date.today()
+
+ # ---------- 构造时间范围 ----------
+ if granularity == 'day':
+ if not year or not month:
+ return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
+ start_date = date(year, month, 1)
+ if month == 12:
+ end_date = date(year + 1, 1, 1)
+ else:
+ end_date = date(year, month + 1, 1)
+ trunc_func = TruncDate('create_time')
+ if not summary_date:
+ summary_date = now.strftime('%Y-%m-%d')
+ summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
+ elif granularity == 'month':
+ if not year:
+ return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
+ start_date = date(year, 1, 1)
+ end_date = date(year + 1, 1, 1)
+ trunc_func = TruncMonth('create_time')
+ if not summary_date:
+ summary_date = now.strftime('%Y-%m')
+ summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
+ elif granularity == 'year':
+ year = year or now.year
+ start_date = date(year, 1, 1)
+ end_date = date(year + 1, 1, 1)
+ trunc_func = TruncYear('create_time')
+ if not summary_date:
+ summary_date = str(year)
+ summary_q = date(year, 1, 1)
+ else:
+ return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
+
+ # 生成 naive datetime(避免 USE_TZ=False 报错)
+ start_dt = datetime.combine(start_date, datetime.min.time())
+ end_dt = datetime.combine(end_date, datetime.min.time())
+
+ # ---------- 充值过滤 ----------
+ chongzhi_filter = {
+ 'huiyuan_id': huiyuan_id, # 会员ID筛选
+ 'leixing': 1,
+ 'zhuangtai': 3,
+ 'create_time__gte': start_dt,
+ 'create_time__lt': end_dt
+ }
+
+ # ---------- 分红过滤 ----------
+ fenhong_filter = {
+ 'huiyuan_id': huiyuan_id, # 分红表会员ID筛选
+ 'create_time__gte': start_dt,
+ 'create_time__lt': end_dt
+ }
+
+ # 充值分组查询
+ chongzhi_qs = Czjilu.objects.filter(**chongzhi_filter) \
+ .annotate(period=trunc_func) \
+ .values('period') \
+ .annotate(
+ chongzhi_count=Count('id'),
+ chongzhi_amount=Sum('jine')
+ ).order_by('period')
+
+ # 分红分组查询
+ fenhong_qs = None
+ if include_guanshi or include_zuzhang:
+ fenhong_agg = {}
+ if include_guanshi:
+ fenhong_agg['guanshi_amount'] = Sum('fenhong')
+ if include_zuzhang:
+ fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong')
+ fenhong_qs = Gsfenhong.objects.filter(**fenhong_filter) \
+ .annotate(period=trunc_func) \
+ .values('period') \
+ .annotate(**fenhong_agg) \
+ .order_by('period')
+
+ # 合并数据
+ data_map = {}
+ for item in chongzhi_qs:
+ period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
+ item['period'].strftime('%Y-%m') if granularity == 'month' else \
+ item['period'].strftime('%Y')
+ data_map[period_str] = {
+ 'chongzhi_count': item['chongzhi_count'],
+ 'chongzhi_amount': float(item['chongzhi_amount'] or 0)
+ }
+
+ if fenhong_qs:
+ for item in fenhong_qs:
+ period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
+ item['period'].strftime('%Y-%m') if granularity == 'month' else \
+ item['period'].strftime('%Y')
+ if period_str not in data_map:
+ data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0}
+ if include_guanshi:
+ data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0)
+ if include_zuzhang:
+ data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0)
+
+ time_series = []
+ for period_str, vals in data_map.items():
+ guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0
+ zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0
+ vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2)
+ vals['date'] = period_str
+ time_series.append(vals)
+ time_series.sort(key=lambda x: x['date'])
+
+ # ---------- 汇总数据 ----------
+ summary_chongzhi_filter = chongzhi_filter.copy()
+ if granularity == 'day':
+ summary_chongzhi_filter['create_time__date'] = summary_q
+ elif granularity == 'month':
+ summary_chongzhi_filter['create_time__year'] = summary_q.year
+ summary_chongzhi_filter['create_time__month'] = summary_q.month
+ else:
+ summary_chongzhi_filter['create_time__year'] = summary_q.year
+
+ summary_chongzhi = Czjilu.objects.filter(**summary_chongzhi_filter).aggregate(
+ total_count=Count('id'),
+ total_amount=Sum('jine')
+ )
+ total_chongzhi_count = summary_chongzhi['total_count'] or 0
+ total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0)
+
+ total_guanshi = 0.0
+ total_zuzhang = 0.0
+ if include_guanshi or include_zuzhang:
+ summary_fenhong_filter = fenhong_filter.copy()
+ if granularity == 'day':
+ summary_fenhong_filter['create_time__date'] = summary_q
+ elif granularity == 'month':
+ summary_fenhong_filter['create_time__year'] = summary_q.year
+ summary_fenhong_filter['create_time__month'] = summary_q.month
+ else:
+ summary_fenhong_filter['create_time__year'] = summary_q.year
+
+ fenhong_agg = {}
+ if include_guanshi:
+ fenhong_agg['guanshi_sum'] = Sum('fenhong')
+ if include_zuzhang:
+ fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong')
+ summary_fenhong = Gsfenhong.objects.filter(**summary_fenhong_filter).aggregate(**fenhong_agg)
+ total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0)
+ total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0)
+
+ total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2)
+
+ return Response({
+ 'code': 0,
+ 'msg': 'ok',
+ 'data': {
+ 'time_series': time_series,
+ 'summary': {
+ 'total_chongzhi_count': total_chongzhi_count,
+ 'total_chongzhi_amount': total_chongzhi_amount,
+ 'total_guanshi_fenhong': total_guanshi,
+ 'total_zuzhang_fenhong': total_zuzhang,
+ 'total_shouyi': total_shouyi
+ }
+ }
+ })
+ except Exception as e:
+ logger.exception("HybkjtsjView 接口错误")
+ return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
+
+
+
+
+
+class SzxxView(APIView):
+ """
+ 每日收支详细统计数据
+ POST /houtai/szxx
+ Body: {
+ "phone": "管理员手机号",
+ "granularity": "day" | "month" | "year",
+ "year": 2026, // day/month 时必填
+ "month": 5, // day 时必填
+ "summary_date": "2026-05-16" // 可选,格式随颗粒度
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 避免模块名冲突,局部导入
+ username_from_frontend = request.data.get('phone', None)
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+ 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')
+
+ now = date.today()
+
+ # ---------- 时间范围与分组字段 ----------
+ if granularity == 'day':
+ if not year or not month:
+ return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
+ # 分组字段
+ group_field = 'day'
+ # 过滤条件
+ income_filter = {'year': year, 'month': month}
+ payout_filter = {'year': year, 'month': month}
+ # 默认汇总日期
+ if not summary_date:
+ summary_date = now.strftime('%Y-%m-%d')
+ summary_parts = summary_date.split('-')
+ summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1]), 'day': int(summary_parts[2])}
+ # 用于构造返回的日期字符串
+ date_format = '%Y-%m-%d'
+
+ elif granularity == 'month':
+ if not year:
+ return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
+ group_field = 'month'
+ income_filter = {'year': year}
+ payout_filter = {'year': year}
+ if not summary_date:
+ summary_date = now.strftime('%Y-%m')
+ summary_parts = summary_date.split('-')
+ summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1])}
+ date_format = '%Y-%m'
+
+ elif granularity == 'year':
+ group_field = 'year'
+ income_filter = {}
+ payout_filter = {}
+ if not summary_date:
+ summary_date = str(now.year)
+ summary_filter = {'year': int(summary_date)}
+ date_format = '%Y'
+ else:
+ return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
+
+ # ---------- 收入分组查询 ----------
+ income_qs = DailyIncomeStat.objects.filter(**income_filter) \
+ .values(group_field) \
+ .annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \
+ .order_by(group_field)
+
+ # ---------- 支出分组查询 ----------
+ payout_qs = DailyPayoutStat.objects.filter(**payout_filter) \
+ .values(group_field) \
+ .annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) \
+ .order_by(group_field)
+
+ # ---------- 合并数据 ----------
+ data_map = {}
+ for item in income_qs:
+ key = item[group_field]
+ data_map[key] = {
+ 'income': float(item['total_income'] or 0),
+ 'income_count': item['total_count'] or 0,
+ 'payout': 0.0,
+ 'payout_count': 0
+ }
+
+ for item in payout_qs:
+ key = item[group_field]
+ if key not in data_map:
+ data_map[key] = {'income': 0.0, 'income_count': 0, 'payout': 0.0, 'payout_count': 0}
+ data_map[key]['payout'] = float(item['total_payout'] or 0)
+ data_map[key]['payout_count'] = item['total_count'] or 0
+
+ time_series = []
+ for key, vals in data_map.items():
+ # 构造日期字符串
+ if granularity == 'day':
+ date_str = f"{year}-{month:02d}-{key:02d}"
+ elif granularity == 'month':
+ date_str = f"{year}-{key:02d}"
+ else:
+ date_str = str(key)
+ profit = round(vals['income'] - vals['payout'], 2)
+ time_series.append({
+ 'date': date_str,
+ 'income': vals['income'],
+ 'payout': vals['payout'],
+ 'profit': profit
+ })
+ time_series.sort(key=lambda x: x['date'])
+
+ # ---------- 汇总数据 ----------
+ income_summary = DailyIncomeStat.objects.filter(**summary_filter).aggregate(
+ total_income=Sum('total_amount'), total_count=Sum('total_count')
+ )
+ payout_summary = DailyPayoutStat.objects.filter(**summary_filter).aggregate(
+ total_payout=Sum('total_amount'), total_count=Sum('total_count')
+ )
+ total_income = float(income_summary['total_income'] or 0)
+ total_payout = float(payout_summary['total_payout'] or 0)
+ total_profit = round(total_income - total_payout, 2)
+
+ return Response({
+ 'code': 0,
+ 'msg': 'ok',
+ 'data': {
+ 'time_series': time_series,
+ 'summary': {
+ 'total_income': total_income,
+ 'total_income_count': income_summary['total_count'] or 0,
+ 'total_payout': total_payout,
+ 'total_payout_count': payout_summary['total_count'] or 0,
+ 'total_profit': total_profit
+ }
+ }
+ })
+
+ except Exception as e:
+ logger.exception("SzxxView 接口错误")
+ return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
+
+
+
+
+
+
+
+
+class CwddhqlxView(APIView):
+ """
+ 获取订单类型(商品类型)用于筛选
+ POST /houtai/cwddhqlx
+ Body: { "phone": "管理员手机号" }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ username_from_frontend = request.data.get('phone', None)
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+ if kefu_obj is None:
+ return permissions
+
+ if 'dingdancaiwu' not in permissions:
+ return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
+
+ # 获取非审核状态(shenhezhuangtai=1)的商品类型,只返回ID和名称
+ types = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values('id', 'jieshao')
+ return Response({
+ 'code': 0,
+ 'msg': 'ok',
+ 'data': list(types)
+ })
+ except Exception as e:
+ logger.exception("CwddhqlxView 接口错误")
+ return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
+
+
+
+
+class CwhqjtddsjView(APIView):
+ """
+ 订单详细统计数据(修正版)
+ POST /houtai/cwhqjtddsj
+ Body: {
+ "phone": "管理员手机号",
+ "granularity": "day" | "month" | "year",
+ "year": 2026,
+ "month": 5,
+ "type_id": 1, # 商品类型ID,0表示全部
+ "order_source": 1, # 1=平台单,2=商家单,0=全部
+ "summary_date": "2026-05-16"
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ username_from_frontend = request.data.get('phone', None)
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+ if kefu_obj is None:
+ return permissions
+ if 'dingdancaiwu' 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')
+ type_id = request.data.get('type_id', 0)
+ order_source = request.data.get('order_source', 0)
+ summary_date = request.data.get('summary_date')
+
+ now = date.today()
+
+ # ---------- 时间范围 ----------
+ if granularity == 'day':
+ if not year or not month:
+ return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
+ start_date = date(year, month, 1)
+ end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
+ trunc_func = TruncDate('create_time')
+ if not summary_date:
+ summary_date = now.strftime('%Y-%m-%d')
+ summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
+ elif granularity == 'month':
+ if not year:
+ return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
+ start_date = date(year, 1, 1)
+ end_date = date(year + 1, 1, 1)
+ trunc_func = TruncMonth('create_time')
+ if not summary_date:
+ summary_date = now.strftime('%Y-%m')
+ summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
+ elif granularity == 'year':
+ year = year or now.year
+ start_date = date(year, 1, 1)
+ end_date = date(year + 1, 1, 1)
+ trunc_func = TruncYear('create_time')
+ if not summary_date:
+ summary_date = str(year)
+ summary_q = date(year, 1, 1)
+ else:
+ return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
+
+ start_dt = datetime.combine(start_date, datetime.min.time())
+ end_dt = datetime.combine(end_date, datetime.min.time())
+
+ # ---------- 基础过滤 ----------
+ filters = {
+ 'create_time__gte': start_dt,
+ 'create_time__lt': end_dt,
+ 'zhuangtai__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态
+ }
+ if int(type_id) != 0:
+ filters['leixing_id'] = type_id
+ if int(order_source) in [1, 2]:
+ filters['fadan_pingtai'] = order_source
+
+ # ---------- 时间序列分组查询 ----------
+ qs = Dingdan.objects.filter(**filters) \
+ .annotate(period=trunc_func) \
+ .values('period') \
+ .annotate(
+ # 订单总量(所有符合条件的状态)
+ order_count=Count('id'),
+ # 订单总额
+ order_amount=Sum('jine'),
+ # 成交单量 (zhuangtai=3)
+ success_count=Count('id', filter=Q(zhuangtai=3)),
+ # 成交订单总额
+ success_amount=Sum('jine', filter=Q(zhuangtai=3)),
+ # 退款单量 (zhuangtai=5)
+ refund_count=Count('id', filter=Q(zhuangtai=5)),
+ # 打手分成总额(仅成交订单)
+ dashou_fencheng_sum=Sum('dashou_fencheng', filter=Q(zhuangtai=3)),
+ # 平台成交订单的店铺分红
+ dianpu_fenhong_sum=Sum('pingtai_kuozhan__dianpu_shouyi',
+ filter=Q(zhuangtai=3, fadan_pingtai=1)),
+ # 成交订单中:平台单金额、商家单金额、各自打手分成
+ platform_success_amount=Sum('jine', filter=Q(zhuangtai=3, fadan_pingtai=1)),
+ merchant_success_amount=Sum('jine', filter=Q(zhuangtai=3, fadan_pingtai=2)),
+ platform_dashou=Sum('dashou_fencheng', filter=Q(zhuangtai=3, fadan_pingtai=1)),
+ merchant_dashou=Sum('dashou_fencheng', filter=Q(zhuangtai=3, fadan_pingtai=2)),
+ ).order_by('period')
+
+ time_series = []
+ for item in qs:
+ period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
+ item['period'].strftime('%Y-%m') if granularity == 'month' else \
+ item['period'].strftime('%Y')
+
+ # 平台收益计算
+ platform_profit = (item['platform_success_amount'] or 0) \
+ - (item['platform_dashou'] or 0) \
+ - (item['dianpu_fenhong_sum'] or 0)
+ merchant_profit = (item['merchant_success_amount'] or 0) \
+ - (item['merchant_dashou'] or 0)
+ total_profit = platform_profit + merchant_profit
+
+ time_series.append({
+ 'date': period_str,
+ 'order_count': item['order_count'],
+ 'order_amount': float(item['order_amount'] or 0),
+ 'success_count': item['success_count'],
+ 'success_amount': float(item['success_amount'] or 0),
+ 'refund_count': item['refund_count'],
+ 'dashou_fencheng': float(item['dashou_fencheng_sum'] or 0),
+ 'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0),
+ 'profit': round(total_profit, 2)
+ })
+
+ # ---------- 汇总数据 ----------
+ summary_qs = Dingdan.objects.filter(**filters)
+ total_order_count = summary_qs.count()
+ total_order_amount = summary_qs.aggregate(s=Sum('jine'))['s'] or 0
+ total_success_count = summary_qs.filter(zhuangtai=3).count()
+ total_success_amount = summary_qs.filter(zhuangtai=3).aggregate(s=Sum('jine'))['s'] or 0
+ total_refund_count = summary_qs.filter(zhuangtai=5).count()
+ total_dashou = summary_qs.filter(zhuangtai=3).aggregate(s=Sum('dashou_fencheng'))['s'] or 0
+ total_dianpu_fenhong = summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(
+ s=Sum('pingtai_kuozhan__dianpu_shouyi'))['s'] or 0
+
+ platform_amount = summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(s=Sum('jine'))['s'] or 0
+ merchant_amount = summary_qs.filter(zhuangtai=3, fadan_pingtai=2).aggregate(s=Sum('jine'))['s'] or 0
+ platform_dashou_sum = \
+ summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(s=Sum('dashou_fencheng'))['s'] or 0
+ merchant_dashou_sum = \
+ summary_qs.filter(zhuangtai=3, fadan_pingtai=2).aggregate(s=Sum('dashou_fencheng'))['s'] or 0
+
+ profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
+ profit_merchant = merchant_amount - merchant_dashou_sum
+ total_profit = profit_platform + profit_merchant
+
+ summary = {
+ 'total_order_count': total_order_count,
+ 'total_order_amount': round(float(total_order_amount), 2),
+ 'total_success_count': total_success_count,
+ 'total_success_amount': round(float(total_success_amount), 2),
+ 'total_refund_count': total_refund_count,
+ 'total_dashou_fencheng': round(float(total_dashou), 2),
+ 'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2),
+ 'total_profit': round(total_profit, 2)
+ }
+
+ return Response({
+ 'code': 0,
+ 'msg': 'ok',
+ 'data': {
+ 'time_series': time_series,
+ 'summary': summary
+ }
+ })
+ except Exception as e:
+ logger.exception("CwhqjtddsjView 接口错误")
+ return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
+
+
+
+
+
+
+
+
+
+
+class CwqtczhqView(APIView):
+ """
+ 其他充值/罚款数据统计(非会员充值)
+ POST /houtai/cwqtczhq
+ Body: {
+ "phone": "管理员手机号",
+ "granularity": "day" | "month" | "year",
+ "year": 2026,
+ "month": 5, // day时必填
+ "types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
+ "summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ username_from_frontend = request.data.get('phone', None)
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+ if kefu_obj is None:
+ return permissions
+ if 'qtczcaiwu' 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')
+ types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部
+ summary_date = request.data.get('summary_date')
+
+ if isinstance(types, list) and len(types) == 0:
+ types = [2, 3, 4, 5, 6]
+
+ now = date.today()
+
+ # ---------- 时间范围(用于曲线图) ----------
+ if granularity == 'day':
+ if not year or not month:
+ return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
+ start_date = date(year, month, 1)
+ end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
+ trunc_func = TruncDate('create_time')
+ # 默认汇总日期
+ if not summary_date:
+ summary_date = now.strftime('%Y-%m-%d')
+ summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
+ elif granularity == 'month':
+ if not year:
+ return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
+ start_date = date(year, 1, 1)
+ end_date = date(year + 1, 1, 1)
+ trunc_func = TruncMonth('create_time')
+ if not summary_date:
+ summary_date = now.strftime('%Y-%m')
+ summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
+ elif granularity == 'year':
+ year = year or now.year
+ start_date = date(year, 1, 1)
+ end_date = date(year + 1, 1, 1)
+ trunc_func = TruncYear('create_time')
+ if not summary_date:
+ summary_date = str(year)
+ summary_q = date(year, 1, 1)
+ else:
+ return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
+
+ start_dt = datetime.combine(start_date, datetime.min.time())
+ end_dt = datetime.combine(end_date, datetime.min.time())
+
+ # ---------- 构建时间序列 ----------
+ time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} }
+
+ def ensure_date(ds):
+ if ds not in time_series_map:
+ time_series_map[ds] = {}
+ return time_series_map[ds]
+
+ # 1. 处理充值记录(Czjilu)—— 完全不变
+ cz_types = [t for t in types if t in [2, 3, 4, 5]]
+ if cz_types:
+ cz_qs = Czjilu.objects.filter(
+ create_time__gte=start_dt,
+ create_time__lt=end_dt,
+ leixing__in=cz_types,
+ zhuangtai=3 # 只统计支付成功的
+ ).annotate(
+ period=trunc_func
+ ).values('period', 'leixing').annotate(
+ total_count=Count('id'),
+ total_amount=Sum('jine')
+ ).order_by('period', 'leixing')
+
+ for item in cz_qs:
+ period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
+ item['period'].strftime('%Y-%m') if granularity == 'month' else \
+ item['period'].strftime('%Y')
+ lx = item['leixing']
+ amount = float(item['total_amount'] or 0)
+ count = item['total_count']
+ # 收益:积分(3)收益为金额本身,其他为0
+ profit = amount if lx == 3 else 0.0
+
+ day_data = ensure_date(period_str)
+ day_data[str(lx)] = {
+ 'count': count,
+ 'amount': amount,
+ 'profit': profit,
+ 'fenhong_count': 0, # 充值类型无分红
+ 'fenhong_amount': 0.0
+ }
+
+ # 2. 处理罚款(Fadan)—— 修改:直接从 Fadan 表获取分红总额和分红笔数
+ if 6 in types:
+ # 使用一次聚合查询,同时获取罚款笔数、罚款金额、分红总额、分红笔数
+ penalty_qs = Fadan.objects.filter(
+ create_time__gte=start_dt,
+ create_time__lt=end_dt,
+ zhuangtai=2 # 已缴纳
+ ).annotate(
+ period=trunc_func
+ ).values('period').annotate(
+ penalty_count=Count('id'),
+ penalty_amount=Sum('fakuanjine'),
+ # 直接使用罚单表中的申请人分红金额字段
+ fenhong_amount=Sum('shenqingren_fenhong_jine'),
+ fenhong_count=Count('id', filter=Q(shenqingren_fenhong_jine__gt=0))
+ ).order_by('period')
+
+ for item in penalty_qs:
+ period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
+ item['period'].strftime('%Y-%m') if granularity == 'month' else \
+ item['period'].strftime('%Y')
+ day_data = ensure_date(period_str)
+ penalty_amt = float(item['penalty_amount'] or 0)
+ fenhong_amt = float(item['fenhong_amount'] or 0)
+ profit = round(penalty_amt - fenhong_amt, 2)
+ day_data['6'] = {
+ 'count': item['penalty_count'],
+ 'amount': penalty_amt,
+ 'profit': profit,
+ 'fenhong_count': item['fenhong_count'],
+ 'fenhong_amount': fenhong_amt
+ }
+
+ # 构造有序时间序列列表
+ time_series = []
+ for period_str in sorted(time_series_map.keys()):
+ entry = {'date': period_str, 'types': {}}
+ for type_code in sorted(time_series_map[period_str].keys()):
+ entry['types'][type_code] = time_series_map[period_str][type_code]
+ time_series.append(entry)
+
+ # ---------- 汇总指定日期(summary)—— 完全保留原逻辑,仅将罚款部分改为从 Fadan 表直接取分红 ----------
+ summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
+
+ # 汇总充值类型(完全不变)
+ for lx in cz_types:
+ filter_kwargs = {
+ 'leixing': lx,
+ 'zhuangtai': 3,
+ }
+ if granularity == 'day':
+ filter_kwargs['create_time__date'] = summary_q
+ elif granularity == 'month':
+ filter_kwargs['create_time__year'] = summary_q.year
+ filter_kwargs['create_time__month'] = summary_q.month
+ else:
+ filter_kwargs['create_time__year'] = summary_q.year
+ agg = Czjilu.objects.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine'))
+ count = agg['count'] or 0
+ amount = float(agg['amount'] or 0)
+ profit = amount if lx == 3 else 0.0
+ summary_result['by_type'][str(lx)] = {
+ 'count': count,
+ 'amount': round(amount, 2),
+ 'profit': round(profit, 2),
+ 'fenhong_count': 0,
+ 'fenhong_amount': 0.0
+ }
+ summary_result['total_count'] += count
+ summary_result['total_amount'] += amount
+ summary_result['total_profit'] += profit
+
+ # 汇总罚款(修改:从 Fadan 表直接取分红,不再查询 FadanFenhong)
+ if 6 in types:
+ penalty_filter = {
+ 'zhuangtai': 2,
+ }
+ if granularity == 'day':
+ penalty_filter['create_time__date'] = summary_q
+ elif granularity == 'month':
+ penalty_filter['create_time__year'] = summary_q.year
+ penalty_filter['create_time__month'] = summary_q.month
+ else:
+ penalty_filter['create_time__year'] = summary_q.year
+
+ # 一次聚合:罚款笔数、金额、分红总额、分红笔数
+ agg = Fadan.objects.filter(**penalty_filter).aggregate(
+ count=Count('id'),
+ amount=Sum('fakuanjine'),
+ fenhong_amount=Sum('shenqingren_fenhong_jine'),
+ fenhong_count=Count('id', filter=Q(shenqingren_fenhong_jine__gt=0))
+ )
+ penalty_count = agg['count'] or 0
+ penalty_amount = float(agg['amount'] or 0)
+ fh_amount = float(agg['fenhong_amount'] or 0)
+ fh_count = agg['fenhong_count'] or 0
+ profit = round(penalty_amount - fh_amount, 2)
+
+ summary_result['by_type']['6'] = {
+ 'count': penalty_count,
+ 'amount': round(penalty_amount, 2),
+ 'profit': profit,
+ 'fenhong_count': fh_count,
+ 'fenhong_amount': round(fh_amount, 2)
+ }
+ summary_result['total_count'] += penalty_count
+ summary_result['total_amount'] += penalty_amount
+ summary_result['total_profit'] += profit
+
+ summary_result['total_amount'] = round(summary_result['total_amount'], 2)
+ summary_result['total_profit'] = round(summary_result['total_profit'], 2)
+
+ return Response({
+ 'code': 0,
+ 'msg': 'ok',
+ 'data': {
+ 'time_series': time_series,
+ 'summary': summary_result
+ }
+ })
+ except Exception as e:
+ logger.exception("CwqtczhqView 接口错误")
+ return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
+
+'''class CwqtczhqView(APIView):
+ """
+ 其他充值/罚款数据统计(非会员充值)
+ POST /houtai/cwqtczhq
+ Body: {
+ "phone": "管理员手机号",
+ "granularity": "day" | "month" | "year",
+ "year": 2026,
+ "month": 5, // day时必填
+ "types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
+ "summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ username_from_frontend = request.data.get('phone', None)
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+ if kefu_obj is None:
+ return permissions
+ if 'qtczcaiwu' 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')
+ types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部
+ summary_date = request.data.get('summary_date')
+
+ if isinstance(types, list) and len(types) == 0:
+ types = [2, 3, 4, 5, 6]
+
+ now = date.today()
+
+ # ---------- 时间范围(用于曲线图) ----------
+ if granularity == 'day':
+ if not year or not month:
+ return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
+ start_date = date(year, month, 1)
+ end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
+ trunc_func = TruncDate('create_time')
+ # 默认汇总日期
+ if not summary_date:
+ summary_date = now.strftime('%Y-%m-%d')
+ summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
+ elif granularity == 'month':
+ if not year:
+ return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
+ start_date = date(year, 1, 1)
+ end_date = date(year + 1, 1, 1)
+ trunc_func = TruncMonth('create_time')
+ if not summary_date:
+ summary_date = now.strftime('%Y-%m')
+ summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
+ elif granularity == 'year':
+ year = year or now.year
+ start_date = date(year, 1, 1)
+ end_date = date(year + 1, 1, 1)
+ trunc_func = TruncYear('create_time')
+ if not summary_date:
+ summary_date = str(year)
+ summary_q = date(year, 1, 1)
+ else:
+ return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
+
+ start_dt = datetime.combine(start_date, datetime.min.time())
+ end_dt = datetime.combine(end_date, datetime.min.time())
+
+ # ---------- 构建时间序列 ----------
+ time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} }
+
+ def ensure_date(ds):
+ if ds not in time_series_map:
+ time_series_map[ds] = {}
+ return time_series_map[ds]
+
+ # 1. 处理充值记录(Czjilu)
+ cz_types = [t for t in types if t in [2, 3, 4, 5]]
+ if cz_types:
+ cz_qs = Czjilu.objects.filter(
+ create_time__gte=start_dt,
+ create_time__lt=end_dt,
+ leixing__in=cz_types,
+ zhuangtai=3 # 只统计支付成功的
+ ).annotate(
+ period=trunc_func
+ ).values('period', 'leixing').annotate(
+ total_count=Count('id'),
+ total_amount=Sum('jine')
+ ).order_by('period', 'leixing')
+
+ for item in cz_qs:
+ period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
+ item['period'].strftime('%Y-%m') if granularity == 'month' else \
+ item['period'].strftime('%Y')
+ lx = item['leixing']
+ amount = float(item['total_amount'] or 0)
+ count = item['total_count']
+ # 收益:积分(3)收益为金额本身,其他为0
+ profit = amount if lx == 3 else 0.0
+
+ day_data = ensure_date(period_str)
+ day_data[str(lx)] = {
+ 'count': count,
+ 'amount': amount,
+ 'profit': profit,
+ 'fenhong_count': 0, # 充值类型无分红
+ 'fenhong_amount': 0.0
+ }
+
+ # 2. 处理罚款(Fadan)—— 包含分红
+ if 6 in types:
+ # 罚款记录
+ penalty_qs = Fadan.objects.filter(
+ create_time__gte=start_dt,
+ create_time__lt=end_dt,
+ zhuangtai=2 # 已缴纳
+ ).annotate(
+ period=trunc_func
+ ).values('period').annotate(
+ penalty_count=Count('id'),
+ penalty_amount=Sum('fakuanjine')
+ ).order_by('period')
+
+ # 分红记录
+ fenhong_qs = FadanFenhong.objects.filter(
+ create_time__gte=start_dt,
+ create_time__lt=end_dt
+ ).annotate(
+ period=trunc_func
+ ).values('period').annotate(
+ fh_count=Count('id'),
+ fh_amount=Sum('ketixian_fenhong')
+ ).order_by('period')
+
+ # 合并
+ for item in penalty_qs:
+ period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
+ item['period'].strftime('%Y-%m') if granularity == 'month' else \
+ item['period'].strftime('%Y')
+ day_data = ensure_date(period_str)
+ day_data['6'] = {
+ 'count': item['penalty_count'],
+ 'amount': float(item['penalty_amount'] or 0),
+ 'profit': 0.0, # 先设为0,后续计算
+ 'fenhong_count': 0,
+ 'fenhong_amount': 0.0
+ }
+
+ for item in fenhong_qs:
+ period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
+ item['period'].strftime('%Y-%m') if granularity == 'month' else \
+ item['period'].strftime('%Y')
+ day_data = ensure_date(period_str)
+ if '6' not in day_data:
+ day_data['6'] = {'count': 0, 'amount': 0.0, 'profit': 0.0, 'fenhong_count': 0, 'fenhong_amount': 0.0}
+ day_data['6']['fenhong_count'] += item['fh_count']
+ day_data['6']['fenhong_amount'] += float(item['fh_amount'] or 0)
+
+ # 计算罚款收益 = 罚款金额 - 分红金额
+ for period_str, day_data in time_series_map.items():
+ if '6' in day_data:
+ penalty = day_data['6']
+ penalty['profit'] = round(penalty['amount'] - penalty['fenhong_amount'], 2)
+
+ # 构造有序时间序列列表
+ time_series = []
+ for period_str in sorted(time_series_map.keys()):
+ entry = {'date': period_str, 'types': {}}
+ for type_code in sorted(time_series_map[period_str].keys()):
+ entry['types'][type_code] = time_series_map[period_str][type_code]
+ time_series.append(entry)
+
+ # ---------- 汇总指定日期 ----------
+ summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
+
+ # 汇总充值类型
+ for lx in cz_types:
+ filter_kwargs = {
+ 'leixing': lx,
+ 'zhuangtai': 3,
+ }
+ if granularity == 'day':
+ filter_kwargs['create_time__date'] = summary_q
+ elif granularity == 'month':
+ filter_kwargs['create_time__year'] = summary_q.year
+ filter_kwargs['create_time__month'] = summary_q.month
+ else:
+ filter_kwargs['create_time__year'] = summary_q.year
+ agg = Czjilu.objects.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine'))
+ count = agg['count'] or 0
+ amount = float(agg['amount'] or 0)
+ profit = amount if lx == 3 else 0.0
+ summary_result['by_type'][str(lx)] = {
+ 'count': count,
+ 'amount': round(amount, 2),
+ 'profit': round(profit, 2),
+ 'fenhong_count': 0,
+ 'fenhong_amount': 0.0
+ }
+ summary_result['total_count'] += count
+ summary_result['total_amount'] += amount
+ summary_result['total_profit'] += profit
+
+ # 汇总罚款
+ if 6 in types:
+ penalty_filter = {
+ 'zhuangtai': 2,
+ }
+ fenhong_filter = {}
+ if granularity == 'day':
+ penalty_filter['create_time__date'] = summary_q
+ fenhong_filter['create_time__date'] = summary_q
+ elif granularity == 'month':
+ penalty_filter['create_time__year'] = summary_q.year
+ penalty_filter['create_time__month'] = summary_q.month
+ fenhong_filter['create_time__year'] = summary_q.year
+ fenhong_filter['create_time__month'] = summary_q.month
+ else:
+ penalty_filter['create_time__year'] = summary_q.year
+ fenhong_filter['create_time__year'] = summary_q.year
+
+ penalty_agg = Fadan.objects.filter(**penalty_filter).aggregate(
+ count=Count('id'), amount=Sum('fakuanjine'))
+ penalty_count = penalty_agg['count'] or 0
+ penalty_amount = float(penalty_agg['amount'] or 0)
+
+ fenhong_agg = FadanFenhong.objects.filter(**fenhong_filter).aggregate(
+ fh_count=Count('id'), fh_amount=Sum('ketixian_fenhong'))
+ fh_count = fenhong_agg['fh_count'] or 0
+ fh_amount = float(fenhong_agg['fh_amount'] or 0)
+
+ profit = round(penalty_amount - fh_amount, 2)
+ summary_result['by_type']['6'] = {
+ 'count': penalty_count,
+ 'amount': round(penalty_amount, 2),
+ 'profit': profit,
+ 'fenhong_count': fh_count,
+ 'fenhong_amount': round(fh_amount, 2)
+ }
+ summary_result['total_count'] += penalty_count
+ summary_result['total_amount'] += penalty_amount
+ summary_result['total_profit'] += profit
+
+ summary_result['total_amount'] = round(summary_result['total_amount'], 2)
+ summary_result['total_profit'] = round(summary_result['total_profit'], 2)
+
+ return Response({
+ 'code': 0,
+ 'msg': 'ok',
+ 'data': {
+ 'time_series': time_series,
+ 'summary': summary_result
+ }
+ })
+ except Exception as e:
+ logger.exception("CwqtczhqView 接口错误")
+ return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)'''
+
+
+
+
+
+# ==================== 后端接口 ====================
+# 文件:views.py (添加以下两个视图类)
+
+
+
+class KhpzhqView(APIView):
+ """
+ 获取所有考核配置数据(称号列表、板块、费用)
+ POST /houtai/khpzhq
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ username_from_frontend = request.data.get('phone', None)
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+ if kefu_obj is None:
+ return permissions
+ if 'kaohepeizhi' not in permissions:
+ return Response({'code': 403, 'msg': '无考核配置权限'}, status=403)
+
+ bankuai_list = list(Bankuai.objects.values('bankuai_id', 'mingcheng'))
+
+ chenghao_qs = Chenghao.objects.prefetch_related('kaohecishufeiyong_set').all()
+ chenghao_list = []
+ for ch in chenghao_qs:
+ # 获取费用:按次数排序,转为 {cishu: feiyong} 字典
+ feiyong_dict = {
+ item.cishu: float(item.feiyong)
+ for item in ch.kaohecishufeiyong_set.order_by('cishu')
+ }
+ max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0
+
+ chenghao_list.append({
+ 'id': ch.id,
+ 'mingcheng': ch.mingcheng,
+ 'leixing': ch.leixing,
+ 'leixing_name': ch.get_leixing_display(),
+ 'bankuai_id': ch.bankuai_id,
+ 'tubiao_url': ch.tubiao_url or '',
+ 'texiao_miaoshu': ch.texiao_miaoshu or '',
+ 'create_time': ch.create_time.strftime('%Y-%m-%d %H:%M:%S') if ch.create_time else '',
+ 'kaohe_guize': ch.kaohe_guize or '',
+ 'kaioi_jinpai': ch.kaioi_jinpai,
+ 'feiyong': feiyong_dict, # {1:100.0, 2:200.0}
+ 'max_cishu': max_cishu
+ })
+
+ return Response({
+ 'code': 0,
+ 'msg': 'ok',
+ 'data': {
+ 'bankuai': bankuai_list,
+ 'chenghao': chenghao_list
+ }
+ })
+ except Exception as e:
+ logger.exception("KhpzhqView 接口错误")
+ return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
+
+
+
+
+class ChzsgcView(APIView):
+ """
+ 称号(标签)增删改查统一接口
+ POST /houtai/chzsgc
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ username_from_frontend = request.data.get('phone', None)
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+ if kefu_obj is None:
+ return permissions
+ if 'kaohepeizhi' not in permissions:
+ return Response({'code': 403, 'msg': '无考核配置权限'}, status=403)
+
+ action = request.data.get('action')
+ if not action:
+ return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
+
+ # ========== 创建称号 ==========
+ if action == 'create':
+ mingcheng = request.data.get('mingcheng', '').strip()
+ leixing = request.data.get('leixing', 'dashou')
+ bankuai_id = request.data.get('bankuai_id')
+ texiao_miaoshu = request.data.get('texiao_miaoshu', '')
+ kaohe_guize = request.data.get('kaohe_guize', '')
+ kaioi_jinpai = request.data.get('kaioi_jinpai', False)
+ feiyong_list = request.data.get('feiyong_list', [])
+
+ if not mingcheng:
+ return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
+ if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
+ return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
+ if Chenghao.objects.filter(mingcheng=mingcheng).exists():
+ return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
+
+ with transaction.atomic():
+ ch = Chenghao.objects.create(
+ mingcheng=mingcheng,
+ leixing=leixing,
+ bankuai_id=bankuai_id,
+ texiao_miaoshu=texiao_miaoshu,
+ kaohe_guize=kaohe_guize,
+ kaioi_jinpai=kaioi_jinpai
+ )
+ # 处理费用:前端传来 [{cishu:1, feiyong:100}, ...]
+ if feiyong_list:
+ for item in feiyong_list:
+ cishu = int(item.get('cishu', 1))
+ feiyong = float(item.get('feiyong', 0))
+ KaoheCishuFeiyong.objects.update_or_create(
+ chenghao=ch, cishu=cishu,
+ defaults={'feiyong': feiyong}
+ )
+ else:
+ # 默认添加第一次免费
+ KaoheCishuFeiyong.objects.create(chenghao=ch, cishu=1, feiyong=0)
+
+ return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}})
+
+ # ========== 修改称号 ==========
+ elif action == 'update':
+ ch_id = request.data.get('chenghao_id')
+ if not ch_id:
+ return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
+ try:
+ ch = Chenghao.objects.get(id=ch_id)
+ except Chenghao.DoesNotExist:
+ return Response({'code': 1, 'msg': '称号不存在'}, status=404)
+
+ mingcheng = request.data.get('mingcheng', ch.mingcheng).strip()
+ leixing = request.data.get('leixing', ch.leixing)
+ bankuai_id = request.data.get('bankuai_id', ch.bankuai_id)
+ texiao_miaoshu = request.data.get('texiao_miaoshu', ch.texiao_miaoshu)
+ kaohe_guize = request.data.get('kaohe_guize', ch.kaohe_guize)
+ kaioi_jinpai = request.data.get('kaioi_jinpai', ch.kaioi_jinpai)
+
+ if not mingcheng:
+ return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
+ if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
+ return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
+ if Chenghao.objects.filter(mingcheng=mingcheng).exclude(id=ch_id).exists():
+ return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
+
+ ch.mingcheng = mingcheng
+ ch.leixing = leixing
+ ch.bankuai_id = bankuai_id
+ ch.texiao_miaoshu = texiao_miaoshu
+ ch.kaohe_guize = kaohe_guize
+ ch.kaioi_jinpai = kaioi_jinpai
+ ch.save()
+
+ # 如果前端传递了 feiyong_list,则全量替换费用
+ feiyong_list = request.data.get('feiyong_list')
+ if feiyong_list is not None:
+ with transaction.atomic():
+ ch.kaohecishufeiyong_set.all().delete()
+ for item in feiyong_list:
+ KaoheCishuFeiyong.objects.create(
+ chenghao=ch,
+ cishu=int(item.get('cishu', 1)),
+ feiyong=float(item.get('feiyong', 0))
+ )
+
+ return Response({'code': 0, 'msg': '修改成功'})
+
+ # ========== 删除称号 ==========
+ elif action == 'delete':
+ ch_id = request.data.get('chenghao_id')
+ if not ch_id:
+ return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
+ try:
+ ch = Chenghao.objects.get(id=ch_id)
+ except Chenghao.DoesNotExist:
+ return Response({'code': 1, 'msg': '称号不存在'}, status=404)
+ ch.delete()
+ return Response({'code': 0, 'msg': '删除成功'})
+
+ # ========== 添加/修改某次费用 ==========
+ elif action == 'add_feiyong':
+ ch_id = request.data.get('chenghao_id')
+ cishu = request.data.get('cishu')
+ feiyong = request.data.get('feiyong', 0)
+ if not ch_id or not cishu:
+ return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
+ try:
+ ch = Chenghao.objects.get(id=ch_id)
+ except Chenghao.DoesNotExist:
+ return Response({'code': 1, 'msg': '称号不存在'}, status=404)
+ if int(cishu) < 1:
+ return Response({'code': 1, 'msg': '次数必须>=1'}, status=400)
+ KaoheCishuFeiyong.objects.update_or_create(
+ chenghao=ch, cishu=cishu,
+ defaults={'feiyong': feiyong}
+ )
+ return Response({'code': 0, 'msg': '添加/修改费用成功'})
+
+ # ========== 删除某次费用 ==========
+ elif action == 'remove_feiyong':
+ ch_id = request.data.get('chenghao_id')
+ cishu = request.data.get('cishu')
+ if not ch_id or not cishu:
+ return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
+ if int(cishu) == 1:
+ return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400)
+ try:
+ ch = Chenghao.objects.get(id=ch_id)
+ except Chenghao.DoesNotExist:
+ return Response({'code': 1, 'msg': '称号不存在'}, status=404)
+ KaoheCishuFeiyong.objects.filter(chenghao=ch, cishu=cishu).delete()
+ return Response({'code': 0, 'msg': '删除成功'})
+
+ # ========== 修改某次费用(等价于add) ==========
+ elif action == 'update_feiyong':
+ return self.add_feiyong(request) # 复用逻辑
+
+ else:
+ return Response({'code': 1, 'msg': '未知操作'}, status=400)
+
+ except Exception as e:
+ logger.exception("ChzsgcView 接口错误")
+ return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
+
+
+
+
+# ==================== 后端 views.py ====================
+# 请确保已有以下导入,如果没有请在文件头部添加
+
+
+
+
+class KhgglView(APIView):
+ """
+ 考核官管理数据接口(列表、筛选、分页)
+ POST /houtai/khggl
+ Body: {
+ "phone": "管理员手机号",
+ "page": 1, // 可选,默认1
+ "page_size": 15, // 可选,默认15
+ "yonghuid": "1234567", // 按用户ID筛选,可选
+ "zhuangtai": 1, // 账号状态 0=禁用,1=正常,可选
+ "shenhe_zhuangtai": 0, // 审核官状态 0=空闲,1=审核中,可选
+ "bankuai_name": "王者" // 板块名称模糊筛选,可选
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ username_from_frontend = request.data.get('phone', None)
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+ if kefu_obj is None:
+ return permissions
+
+ if 'kaohepeizhi' not in permissions:
+ return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
+
+ # 分页参数
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 15))
+ if page < 1: page = 1
+ if page_size < 1: page_size = 15
+
+ # 筛选条件
+ filter_yonghuid = request.data.get('yonghuid', '').strip()
+ filter_zhuangtai = request.data.get('zhuangtai')
+ filter_shenhe_zhuangtai = request.data.get('shenhe_zhuangtai')
+ filter_bankuai_name = request.data.get('bankuai_name', '').strip()
+
+ # 基础查询:所有审核官,预加载用户及打手扩展表
+ queryset = UserShenheguan.objects.select_related('user__dashou_profile')
+
+ if filter_yonghuid:
+ queryset = queryset.filter(user__yonghuid=filter_yonghuid)
+ if filter_zhuangtai is not None:
+ queryset = queryset.filter(zhuangtai=filter_zhuangtai)
+ if filter_shenhe_zhuangtai is not None:
+ queryset = queryset.filter(shenhe_zhuangtai=filter_shenhe_zhuangtai)
+
+ # 如果填写了板块名称,筛选出关联该板块的审核官
+ if filter_bankuai_name:
+ bankuai_ids = Bankuai.objects.filter(
+ mingcheng__icontains=filter_bankuai_name
+ ).values_list('bankuai_id', flat=True)
+ shenheguan_ids = KaoheguanBankuai.objects.filter(
+ bankuai_id__in=bankuai_ids
+ ).values_list('kaoheguan_id', flat=True)
+ queryset = queryset.filter(user__yonghuid__in=shenheguan_ids)
+
+ total = queryset.count()
+ start = (page - 1) * page_size
+ end = start + page_size
+ shenheguan_list = queryset.order_by('-create_time')[start:end]
+
+ # ---------- 预计算每个板块的打手数量 ----------
+ bankuai_dashou_count_map = {}
+ all_banquai = Bankuai.objects.all()
+ for bk in all_banquai:
+ tag_ids = Chenghao.objects.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True)
+ count = YonghuChenghao.objects.filter(
+ chenghao_id__in=tag_ids,
+ yonghu__dashou_profile__isnull=False
+ ).values('yonghu').distinct().count()
+ bankuai_dashou_count_map[bk.bankuai_id] = count
+
+ # ---------- 新增:板块标签统计 ----------
+ bankuai_tags_data = []
+ for bk in all_banquai:
+ tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou')
+ tag_list = []
+ for tag in tags:
+ user_count = YonghuChenghao.objects.filter(chenghao=tag).values('yonghu').distinct().count()
+ tag_list.append({
+ 'id': tag.id,
+ 'name': tag.mingcheng,
+ 'user_count': user_count,
+ })
+ bankuai_tags_data.append({
+ 'bankuai_id': bk.bankuai_id,
+ 'mingcheng': bk.mingcheng,
+ 'tags': tag_list,
+ })
+
+ # 构造返回数据(审核官列表)
+ data_list = []
+ for sg in shenheguan_list:
+ user = sg.user
+ # 获取该审核官关联的板块
+ bk_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=user.yonghuid).select_related('bankuai')
+ bankuai_info = []
+ for bk in bk_qs:
+ bid = bk.bankuai.bankuai_id
+ bankuai_info.append({
+ 'bankuai_id': bid,
+ 'mingcheng': bk.bankuai.mingcheng,
+ 'dashou_count': bankuai_dashou_count_map.get(bid, 0)
+ })
+
+ dashou_nick = user.dashou_profile.nicheng if hasattr(user, 'dashou_profile') and user.dashou_profile else ''
+
+ data_list.append({
+ 'yonghuid': user.yonghuid,
+ 'avatar': user.avatar or '',
+ 'phone': user.phone or '',
+ 'zhuangtai': sg.zhuangtai,
+ 'shenhe_zhuangtai': sg.shenhe_zhuangtai,
+ 'shenhe_zongshu': sg.shenhe_zongshu,
+ 'tongguo_zongshu': sg.tongguo_zongshu,
+ 'yue': float(sg.yue),
+ 'zonge': float(sg.zonge),
+ 'create_time': sg.create_time.strftime('%Y-%m-%d %H:%M:%S') if sg.create_time else '',
+ 'update_time': sg.update_time.strftime('%Y-%m-%d %H:%M:%S') if sg.update_time else '',
+ 'bankuai': bankuai_info,
+ 'nicheng': dashou_nick,
+ 'is_renzheng': sg.is_renzheng,
+ })
+
+ all_bankuai = list(Bankuai.objects.values('bankuai_id', 'mingcheng'))
+
+ return Response({
+ 'code': 0,
+ 'msg': 'ok',
+ 'data': {
+ 'total': total,
+ 'page': page,
+ 'page_size': page_size,
+ 'list': data_list,
+ 'all_bankuai': all_bankuai,
+ 'bankuai_tags': bankuai_tags_data, # 新增板块标签统计
+ }
+ })
+
+ except Exception as e:
+ logger.exception("KhgglView 接口错误")
+ return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
+
+class ShgxgsjView(APIView):
+ """
+ 审核官信息修改(余额、状态、板块关联)
+ POST /houtai/shgxgsj
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ username_from_frontend = request.data.get('phone', None)
+ kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
+ if kefu_obj is None:
+ return permissions
+ if 'kaohepeizhi' not in permissions:
+ return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
+
+ yonghuid = request.data.get('yonghuid')
+ if not yonghuid:
+ return Response({'code': 1, 'msg': '缺少用户ID'}, status=400)
+
+ try:
+ shenheguan = UserShenheguan.objects.select_related('user').get(user__yonghuid=yonghuid)
+ except UserShenheguan.DoesNotExist:
+ return Response({'code': 1, 'msg': '审核官不存在'}, status=404)
+
+ action = request.data.get('action')
+
+ if action == 'update_info':
+ if 'yue' in request.data:
+ shenheguan.yue = float(request.data.get('yue', 0))
+ if 'zhuangtai' in request.data:
+ zt = int(request.data.get('zhuangtai'))
+ if zt not in [0, 1]:
+ return Response({'code': 1, 'msg': '无效的账号状态'}, status=400)
+ shenheguan.zhuangtai = zt
+ if 'shenhe_zhuangtai' in request.data:
+ szt = int(request.data.get('shenhe_zhuangtai'))
+ if szt not in [0, 1]:
+ return Response({'code': 1, 'msg': '无效的审核官状态'}, status=400)
+ shenheguan.shenhe_zhuangtai = szt
+ # 新增认证状态编辑
+ if 'is_renzheng' in request.data:
+ shenheguan.is_renzheng = bool(request.data.get('is_renzheng'))
+ shenheguan.save()
+ return Response({'code': 0, 'msg': '修改成功'})
+
+ elif action == 'add_bankuai':
+ bankuai_id = request.data.get('bankuai_id')
+ if not bankuai_id:
+ return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
+ if not Bankuai.objects.filter(bankuai_id=bankuai_id).exists():
+ return Response({'code': 1, 'msg': '板块不存在'}, status=400)
+ if KaoheguanBankuai.objects.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists():
+ return Response({'code': 1, 'msg': '已关联该板块'}, status=400)
+ KaoheguanBankuai.objects.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id)
+ return Response({'code': 0, 'msg': '板块关联成功'})
+
+ elif action == 'remove_bankuai':
+ bankuai_id = request.data.get('bankuai_id')
+ if not bankuai_id:
+ return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
+ deleted, _ = KaoheguanBankuai.objects.filter(
+ kaoheguan_id=yonghuid, bankuai_id=bankuai_id
+ ).delete()
+ if not deleted:
+ return Response({'code': 1, 'msg': '未找到该板块关联'}, status=400)
+ return Response({'code': 0, 'msg': '板块移除成功'})
+
+ else:
+ return Response({'code': 1, 'msg': '未知操作'}, status=400)
+
+ except Exception as e:
+ logger.exception("ShgxgsjView 接口错误")
+ return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
+
+
+
+
+
+class ZxkfghdsView(APIView):
+ """
+ 客服更换打手接口
+ 路径:POST /houtai/zxkfghds
+ 功能:
+ 1. 将当前订单状态改为已退款(5),释放接单打手(若有)
+ 2. 如需跨平台通知,则调用对方平台退款通知接口
+ 3. 基于原订单信息生成全新订单(状态直接为 1,已替老板填写)
+ 4. 生成新的派单链接,并标记为已使用
+ 5. 新链接记录上一个链接ID和上一个订单ID
+ 6. 【关键】不操作商家余额、退款次数、每日统计,因新订单是替换而非新增扣款
+ 权限:需要'003aa'(跨平台管理)权限,与退款接口一致
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # ========== 1. 参数提取 ==========
+ username = request.data.get('username', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ if not username or not dingdan_id:
+ logger.warning("更换打手参数不完整")
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # ========== 2. 权限校验(与跨平台退款相同) ==========
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 返回错误响应
+ if '003aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权处理订单'})
+
+ current_user = request.user
+
+ # ========== 3. 查询原订单及关联信息 ==========
+ try:
+ old_order = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id)
+ except Dingdan.DoesNotExist:
+ logger.warning(f"订单不存在: {dingdan_id}")
+ return Response({'code': 404, 'msg': '订单不存在'})
+
+ # 仅允许状态为 2(进行中)或 8(结算中)更换打手
+ if old_order.zhuangtai not in [1,7,2, 8]:
+ return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'})
+
+ # 本接口仅处理商家发单(fadan_pingtai=2)
+ if old_order.fadan_pingtai != 2:
+ return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'})
+
+ # 获取原链接(用于模板ID等)
+ try:
+ old_lianjie = ShangjiaLianjie.objects.get(dingdan_id=dingdan_id)
+ except ShangjiaLianjie.DoesNotExist:
+ logger.error(f"原链接记录不存在,订单: {dingdan_id}")
+ return Response({'code': 500, 'msg': '链接数据异常'})
+
+ # 商家信息(仅用于记录关联,不操作余额)
+ try:
+ shangjia_ext = old_order.shangjia_kuozhan
+ shangjia_id = shangjia_ext.shangjia_id
+ # 获取商家昵称用于新订单扩展表
+ shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
+ shangjia_nicheng = shangjia_user.shop_profile.nicheng or f"商家{shangjia_id}"
+ except (ObjectDoesNotExist, UserShangjia.DoesNotExist):
+ logger.error(f"商家信息缺失,订单: {dingdan_id}")
+ return Response({'code': 500, 'msg': '商家数据异常'})
+
+ # ========== 4. 判断是否需要跨平台退款通知 ==========
+ is_cross = old_order.is_cross
+ partner_club_id = old_order.partner_club_id
+ partner_order_id = old_order.partner_order_id
+ need_notify_partner = is_cross and partner_club_id and partner_order_id
+
+ # ========== 5. 退款处理(仅打手侧,不碰商家) ==========
+ with transaction.atomic():
+ # 5.1 订单状态改为已退款
+ old_order.zhuangtai = 5
+ old_order.clkf = current_user.phone
+ old_order.save()
+
+ # 5.2 处理接单打手(若有)
+ dashou_id = old_order.jiedan_dashou_id
+ if dashou_id:
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ dashou_profile.tuikuanliang += 1 # 退款次数+1
+ dashou_profile.zhuangtai = 1 # 设为空闲
+ dashou_profile.save()
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"打手 {dashou_id} 不存在,跳过更新")
+
+ # 5.3 更新退款记录表
+ Tuikuanjilu.objects.update_or_create(
+ dingdan_id=dingdan_id,
+ defaults={
+ 'dashouid': dashou_id or '',
+ 'qingqiuid': '',
+ 'chuliid': current_user.phone,
+ 'liyou': '客服更换打手退款',
+ 'sqzhuangtai': 1,
+ 'jine': old_order.jine or 0,
+ 'dashou_fencheng': old_order.dashou_fencheng or 0,
+ 'jieshao': old_order.jieshao or '',
+ 'beizhu': '更换打手自动退款',
+ 'nicheng': old_order.nicheng or '',
+ }
+ )
+
+ # 5.4 打手每日统计(退款)
+ if dashou_id:
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=dashou_id,
+ amount=old_order.dashou_fencheng,
+ action=3 # 3=退款
+ )
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+
+ # 5.5 客服处理统计
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ # 5.6 跨平台退款通知(需要时)
+ if need_notify_partner:
+ try:
+ # 获取我方俱乐部ID
+ my_club_config = ClubConfig.objects.filter(is_self=1).first()
+ if not my_club_config:
+ raise Exception("我方俱乐部配置不存在")
+ self_club_id = my_club_config.club_id
+ club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
+ partner_domain = club_rel.partner_domain
+
+ notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk'
+ payload = {
+ 'order_id': partner_order_id,
+ 'club_id': self_club_id,
+ 'status': 5,
+ 'reason': '客服更换打手退款'
+ }
+
+ # 调用对方接口(必须成功,否则整个事务回滚)
+ resp = requests.post(notify_url, json=payload, timeout=5)
+ if resp.status_code == 200:
+ resp_json = resp.json()
+ if resp_json.get('code') != 0:
+ transaction.set_rollback(True)
+ logger.error(f"对方平台退款通知失败: {resp_json.get('msg')}")
+ return Response({'code': 500, 'msg': f'对方平台通知失败: {resp_json.get("msg")}'})
+ else:
+ transaction.set_rollback(True)
+ logger.error(f"对方平台接口HTTP错误: {resp.status_code}")
+ return Response({'code': 500, 'msg': '通知对方平台失败'})
+
+ # 更新跨平台扩展表(如果存在)
+ CrossPlatformOrderData.objects.update_or_create(
+ dingdan_id=dingdan_id,
+ defaults={
+ 'partner_order_id': partner_order_id,
+ 'partner_club_id': partner_club_id,
+ 'partner_order_status': 5,
+ }
+ )
+ except Exception as e:
+ transaction.set_rollback(True)
+ logger.error(f"通知对方平台异常: {e}")
+ return Response({'code': 500, 'msg': f'通知对方平台失败: {str(e)}'})
+
+ # 至此,原订单已退款,事务提交
+
+ # ========== 6. 生成新订单和链接(不扣商家余额) ==========
+ try:
+ with transaction.atomic():
+ # ---- 6.1 生成新的订单ID和Token ----
+ def generate_dingdan_id():
+ timestamp = int(time.time() * 1000)
+ process_id = os.getpid() % 1000
+ random_num = random.randint(1000, 9999)
+ return f"SJ{timestamp}{process_id:03d}{random_num}"
+
+ def generate_secure_token(dd_id, uid):
+ timestamp = int(time.time() * 1_000_000)
+ salt = secrets.token_hex(8)
+ raw = f"{timestamp}:{dd_id}:{uid}:{salt}"
+ token = hashlib.sha256(raw.encode()).hexdigest()[:24]
+ if ShangjiaLianjie.objects.filter(lianjie_token=token).exists():
+ return generate_secure_token(dd_id, uid)
+ return token
+
+ def generate_link_url(token):
+ base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/')
+ return f"{base}/order/{token}"
+
+ new_dingdan_id = generate_dingdan_id()
+ secure_token = generate_secure_token(new_dingdan_id, shangjia_id)
+
+ # ---- 6.2 创建新订单(复制核心数据,状态直接为1) ----
+ new_order = Dingdan.objects.create(
+ dingdan_id=new_dingdan_id,
+ zhuangtai=1, # 下单中
+ fadan_pingtai=2,
+ jine=old_order.jine,
+ dashou_fencheng=old_order.dashou_fencheng,
+ leixing_id=old_order.leixing_id,
+ yaoqiuleixing=old_order.yaoqiuleixing,
+ huiyuan_id=old_order.huiyuan_id,
+ yongjin=old_order.yongjin,
+ jieshao=old_order.jieshao,
+ nicheng=old_order.nicheng, # 继承游戏昵称
+ beizhu=old_order.beizhu,
+ user1_id=old_order.user1_id, # 商家标识
+ tupian=old_order.tupian,
+ # 注意:不继承 zhiding_id、jiedan_dashou_id
+ )
+
+ # ---- 6.3 创建商家扩展表 ----
+ DingdanShangjia.objects.create(
+ dingdan=new_order,
+ shangjia_id=shangjia_id,
+ sjnicheng=shangjia_nicheng,
+ )
+
+ # ---- 6.4 生成新链接并关联旧链接信息 ----
+ link_url = generate_link_url(secure_token)
+ new_lianjie = ShangjiaLianjie.objects.create(
+ yonghu_id=shangjia_id,
+ leixing_id=old_lianjie.leixing_id,
+ moban_id=old_lianjie.moban_id,
+ lianjie=link_url,
+ lianjie_token=secure_token,
+ dingdan_id=new_dingdan_id,
+ is_used=True, # 直接标记已使用
+ youxiao_shijian=timezone.now() + timezone.timedelta(days=1),
+ previous_lianjie_id=old_lianjie.id, # 记录旧链接ID
+ previous_dingdan_id=old_order.dingdan_id, # 记录旧订单ID
+ )
+
+ # ---- 6.5 异步通知(广播新订单、跨平台同步) ----
+ try:
+ order_info = {
+ 'dingdan_id': new_dingdan_id,
+ 'game_type': self._get_game_type_name(new_order.leixing_id),
+ 'amount': str(new_order.jine),
+ 'order_desc': (new_order.jieshao or '')[:50],
+ }
+ dingdan_guangbo.delay(order_info)
+ except Exception as e:
+ logger.error(f"广播通知失败: {e}")
+
+ # 跨平台同步(与客户填写接口保持一致)
+ try:
+ sync_order_to_partners(new_dingdan_id)
+ except Exception as e:
+ logger.error(f"跨平台同步失败: {e}")
+
+ # 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单
+
+ logger.info(f"更换打手成功:旧订单{dingdan_id}→退款,新订单{new_dingdan_id}已生成")
+
+ return Response({
+ 'code': 0,
+ 'msg': '更换成功',
+ 'data': {
+ 'new_dingdan_id': new_dingdan_id,
+ 'new_lianjie': link_url,
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"生成新订单事务失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '生成新订单失败'})
+
+ def _get_game_type_name(self, leixing_id):
+ """根据类型ID获取游戏类型名称"""
+ try:
+ gt = ShangpinLeixing.objects.filter(id=leixing_id).first()
+ return gt.jieshao if gt else '游戏订单'
+ except Exception:
+ return '游戏订单'
+
+
+
+
+
+
+
+
+
+class KptxwztjbView(APIView):
+ """
+ 跨平台订单状态同步接口(对方平台调用)
+ 请求:POST /houtai/kptxwztjb
+ 参数:
+ - token: 同步专用令牌(明文)
+ - partner_order_id: 我方订单ID(在对方眼中是partner_order_id,实际查本库dingdan_id)
+ - our_order_id: 对方订单ID(在本库中为partner_order_id)
+ - club_id: 俱乐部ID(可选验证)
+ 返回:
+ code=0 成功,data.status 为订单状态
+ 其他 code 为错误
+ 注意:此接口仅用于查询对方订单状态,不做任何修改
+ """
+
+ authentication_classes = [] # 无需JWT等认证
+ permission_classes = [] # 公开接口
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ token = request.data.get('token', '').strip()
+ partner_order_id = request.data.get('partner_order_id', '').strip()
+ our_order_id = request.data.get('our_order_id', '').strip()
+ club_id = request.data.get('club_id', '').strip()
+
+ if not all([token, partner_order_id, our_order_id]):
+ logger.warning("跨平台状态同步参数缺失")
+ return Response({'code': 400, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 验证同步令牌
+ SYNC_TOKEN = getattr(settings, 'CROSS_PLATFORM_SYNC_TOKEN', None)
+ if not SYNC_TOKEN or token != SYNC_TOKEN:
+ logger.warning(f"无效的同步令牌: {token[:8]}...")
+ return Response({'code': 403, 'msg': '无效令牌'}, status=status.HTTP_403_FORBIDDEN)
+
+ # 3. 查询订单
+ # partner_order_id 在对方视角是“我方订单ID”,在本库中应匹配 dingdan_id
+ try:
+ order = Dingdan.objects.get(dingdan_id=partner_order_id)
+ except Dingdan.DoesNotExist:
+ logger.warning(f"订单不存在, partner_order_id: {partner_order_id}")
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 验证订单关联关系:本库中的 partner_order_id 应等于传入的 our_order_id
+ if order.partner_order_id != our_order_id:
+ logger.warning(
+ f"订单关联不匹配: partner_order_id={order.partner_order_id}, our_order_id={our_order_id}"
+ )
+ return Response({'code': 400, 'msg': '订单关联信息不匹配'}, status=status.HTTP_400_BAD_REQUEST)
+
+
+
+ # 6. 返回订单状态
+ status_code = order.zhuangtai
+ logger.info(
+ f"跨平台状态查询成功, dingdan_id={order.dingdan_id}, status={status_code}"
+ )
+ return Response({
+ 'code': 0,
+ 'msg': 'success',
+ 'data': {
+ 'dingdan_id': order.dingdan_id,
+ 'status': status_code,
+ }
})
\ No newline at end of file
diff --git a/dengji/migrations/__init__.py b/config/__init__.py
similarity index 100%
rename from dengji/migrations/__init__.py
rename to config/__init__.py
diff --git a/dengji/admin.py b/config/admin.py
similarity index 100%
rename from dengji/admin.py
rename to config/admin.py
diff --git a/peizhi/apps.py b/config/apps.py
similarity index 100%
rename from peizhi/apps.py
rename to config/apps.py
diff --git a/peizhi/migrations/0001_initial.py b/config/migrations/0001_initial.py
similarity index 100%
rename from peizhi/migrations/0001_initial.py
rename to config/migrations/0001_initial.py
diff --git a/peizhi/migrations/0002_shangjiamoban_shangjialianjie.py b/config/migrations/0002_shangjiamoban_shangjialianjie.py
similarity index 100%
rename from peizhi/migrations/0002_shangjiamoban_shangjialianjie.py
rename to config/migrations/0002_shangjiamoban_shangjialianjie.py
diff --git a/peizhi/migrations/0003_tupianpeizhi_fangwen_cishu.py b/config/migrations/0003_tupianpeizhi_fangwen_cishu.py
similarity index 100%
rename from peizhi/migrations/0003_tupianpeizhi_fangwen_cishu.py
rename to config/migrations/0003_tupianpeizhi_fangwen_cishu.py
diff --git a/peizhi/migrations/0004_tixianquotadefault.py b/config/migrations/0004_tixianquotadefault.py
similarity index 100%
rename from peizhi/migrations/0004_tixianquotadefault.py
rename to config/migrations/0004_tixianquotadefault.py
diff --git a/peizhi/migrations/0005_ordertypemapping_dailydispatchstat_club.py b/config/migrations/0005_ordertypemapping_dailydispatchstat_club.py
similarity index 100%
rename from peizhi/migrations/0005_ordertypemapping_dailydispatchstat_club.py
rename to config/migrations/0005_ordertypemapping_dailydispatchstat_club.py
diff --git a/peizhi/migrations/0006_clubconfig_accountpermission.py b/config/migrations/0006_clubconfig_accountpermission.py
similarity index 100%
rename from peizhi/migrations/0006_clubconfig_accountpermission.py
rename to config/migrations/0006_clubconfig_accountpermission.py
diff --git a/peizhi/migrations/0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more.py b/config/migrations/0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more.py
similarity index 100%
rename from peizhi/migrations/0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more.py
rename to config/migrations/0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more.py
diff --git a/peizhi/migrations/0008_clubconfig_storage_bucket_name.py b/config/migrations/0008_clubconfig_storage_bucket_name.py
similarity index 100%
rename from peizhi/migrations/0008_clubconfig_storage_bucket_name.py
rename to config/migrations/0008_clubconfig_storage_bucket_name.py
diff --git a/peizhi/migrations/0009_dailypayoutstat_dailyincomestat.py b/config/migrations/0009_dailypayoutstat_dailyincomestat.py
similarity index 100%
rename from peizhi/migrations/0009_dailypayoutstat_dailyincomestat.py
rename to config/migrations/0009_dailypayoutstat_dailyincomestat.py
diff --git a/peizhi/migrations/0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more.py b/config/migrations/0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more.py
similarity index 100%
rename from peizhi/migrations/0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more.py
rename to config/migrations/0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more.py
diff --git a/peizhi/migrations/0011_withdrawconfig.py b/config/migrations/0011_withdrawconfig.py
similarity index 100%
rename from peizhi/migrations/0011_withdrawconfig.py
rename to config/migrations/0011_withdrawconfig.py
diff --git a/peizhi/migrations/0012_shangjiamoban_chenghao_id_and_more.py b/config/migrations/0012_shangjiamoban_chenghao_id_and_more.py
similarity index 100%
rename from peizhi/migrations/0012_shangjiamoban_chenghao_id_and_more.py
rename to config/migrations/0012_shangjiamoban_chenghao_id_and_more.py
diff --git a/peizhi/migrations/0013_shangjialianjie_previous_dingdan_id_and_more.py b/config/migrations/0013_shangjialianjie_previous_dingdan_id_and_more.py
similarity index 100%
rename from peizhi/migrations/0013_shangjialianjie_previous_dingdan_id_and_more.py
rename to config/migrations/0013_shangjialianjie_previous_dingdan_id_and_more.py
diff --git a/dingdan/__init__.py b/config/migrations/__init__.py
similarity index 100%
rename from dingdan/__init__.py
rename to config/migrations/__init__.py
diff --git a/peizhi/models.py b/config/models.py
similarity index 99%
rename from peizhi/models.py
rename to config/models.py
index 1715daf..c4ac910 100644
--- a/peizhi/models.py
+++ b/config/models.py
@@ -1,5 +1,7 @@
from django.db import models
+
+
class Lunbo(models.Model):
tupian_url = models.CharField(max_length=500, null=True, blank=True, verbose_name='轮播图片URL')
leixing = models.IntegerField(null=True, blank=True, verbose_name='图片类型:1:轮播2:个人中心背景')
@@ -66,9 +68,6 @@ class Szjilu(models.Model):
-# models/shangjia_moban.py
-from django.db import models
-
class ShangjiaMoban(models.Model):
"""商家订单模板表 - 存储商家创建的订单模板"""
@@ -119,8 +118,6 @@ class ShangjiaMoban(models.Model):
return f"模板{self.id}-{self.yonghu_id}"
-# models/shangjia_lianjie.py
-from django.db import models
class ShangjiaLianjie(models.Model):
@@ -274,10 +271,6 @@ class WithdrawConfig(models.Model):
-
-# apps/club/models.py
-from django.db import models
-
class Club(models.Model):
"""合作俱乐部/平台信息表"""
# 自己的俱乐部ID(6位字符)
@@ -384,8 +377,6 @@ class ClubConfig(models.Model):
return f"{self.club_nickname or self.club_id} ({'我方' if self.is_self else '对方'})"
-from django.db import models
-
class AccountPermission(models.Model):
"""账号权限表:记录每个账号拥有的权限及开关状态"""
account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID')
@@ -521,10 +512,6 @@ class DailyPayoutStat(models.Model):
-
-
-from django.db import models
-
class PopupPage(models.Model):
page_key = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='页面标识')
name = models.CharField(max_length=100, verbose_name='页面名称')
diff --git a/peizhi/serializers.py b/config/serializers.py
similarity index 100%
rename from peizhi/serializers.py
rename to config/serializers.py
diff --git a/peizhi/tasks.py b/config/tasks.py
similarity index 98%
rename from peizhi/tasks.py
rename to config/tasks.py
index f49f474..ee1118a 100644
--- a/peizhi/tasks.py
+++ b/config/tasks.py
@@ -3,7 +3,6 @@
处理收支记录表的每日清零
固定使用ID=1的记录
"""
-
from celery import shared_task
from django.db import transaction
from django.utils import timezone
@@ -12,7 +11,7 @@ from decimal import Decimal
import logging
# 导入模型
-from peizhi.models import Szjilu
+from config.models import Szjilu
from utils.celery_utils import log_task_execution, rollback_on_failure
logger = logging.getLogger(__name__)
diff --git a/dengji/tests.py b/config/tests.py
similarity index 100%
rename from dengji/tests.py
rename to config/tests.py
diff --git a/peizhi/urls.py b/config/urls.py
similarity index 99%
rename from peizhi/urls.py
rename to config/urls.py
index 3dc61e6..0459e8e 100644
--- a/peizhi/urls.py
+++ b/config/urls.py
@@ -37,4 +37,4 @@ urlpatterns = [
path('yhbdsjh', CheckPhoneAuthView.as_view(), name='判断是否需要手机号绑定'),
path('hqsjlslj', ShangjiaLianjieListView.as_view(), name='商家获取生成链接'),
-]
\ No newline at end of file
+]
diff --git a/peizhi/views.py b/config/views.py
similarity index 59%
rename from peizhi/views.py
rename to config/views.py
index f213cf4..f0cb6db 100644
--- a/peizhi/views.py
+++ b/config/views.py
@@ -1,5563 +1,3376 @@
-# peizhi/views.py
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.throttling import AnonRateThrottle
-from django.db import models
-from peizhi.models import Gonggao, Lunbo
-import logging
-from rest_framework.permissions import AllowAny # 新增这行
-
-# 配置日志
-logger = logging.getLogger(__name__)
-
-
-# peizhi/views.py 或新建 views_config.py
-import logging
-from rest_framework.views import APIView
-
-from rest_framework.permissions import AllowAny
-from rest_framework.parsers import JSONParser
-from .models import ClubConfig
-
-
-class GetDynamicConfigView(APIView):
-
-
- permission_classes = [AllowAny]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 查询我方俱乐部配置(is_self=1),取第一条(通常只有一条)
- club_config = ClubConfig.objects.filter(is_self=1).only(
- 'storage_bucket_domain',
- 'storage_bucket_name',
- 'chat_api_key',
- 'customer_service_api_url',
- 'customer_service_corp_id'
- ).first()
-
- # 默认值(防止空数据导致前端报错)
- cos_bucket = ''
- cos_region = 'ap-shanghai'
- cos_oss_url = ''
- goeasy_host = 'hangzhou.goeasy.io'
- goeasy_appkey = ''
- kefu_link = ''
- kefu_enterprise_id = ''
- morentouxiang = 'beijing/morentouxiang.jpg'
- dashouguize = 'a_long/dashouguize.jpg'
-
- if club_config:
- # 存储桶配置
- bucket = club_config.storage_bucket_name or ''
- region = 'ap-shanghai' # 若需要动态,可单独存储
- cos_oss_url = club_config.storage_bucket_domain or ''
- if cos_oss_url and not cos_oss_url.endswith('/'):
- cos_oss_url += '/'
- cos_bucket = bucket
- cos_region = region
-
- # GoEasy 配置
- if club_config.chat_api_key:
- goeasy_appkey = club_config.chat_api_key
- # host 如果需要可动态,这里固定或从其他字段获取
-
- # 客服配置
- if club_config.customer_service_api_url:
- kefu_link = club_config.customer_service_api_url
- if club_config.customer_service_corp_id:
- kefu_enterprise_id = club_config.customer_service_corp_id
-
- # 其他配置可在此扩展
-
- # 构造返回数据
- data = {
- "code": 0,
- "data": {
- "cos": {
- "bucket": cos_bucket,
- "region": cos_region,
- "ossImageUrl": cos_oss_url,
- "uploadPathPrefix": "order/"
- },
- "goEasy": {
- "host": goeasy_host,
- "appkey": goeasy_appkey
- },
- "otherConfig": {
- "morentouxiang": morentouxiang,
- "dashouguize": dashouguize
- },
- "kefu": {
- "link": kefu_link,
- "enterpriseId": kefu_enterprise_id
- }
- }
- }
- return Response(data)
-
- except Exception as e:
- logger.error(f"获取动态配置失败: {str(e)}", exc_info=True)
- return Response({"code": 500, "msg": "服务器内部错误"}, status=500)
-
-
-
-
-
-
-class ShangpinGonggaoView(APIView):
- """
- 商品公告和轮播图接口
- 获取类型为1的公告和轮播图
- """
-
- # 应用节流(限制频率)
- throttle_classes = [AnonRateThrottle]
- # 新增:允许所有用户访问(不需要认证)
- permission_classes = [AllowAny]
-
- def post(self, request):
- """
- 处理POST请求,返回公告和轮播图数据
-
- 请求: POST /api/peizhi/shangpingonggao/
- 返回: {
- "shangpingonggao": "公告内容字符串",
- "shangpinlunbo": ["轮播图URL1", "轮播图URL2"]
- }
- """
- try:
- logger.info("收到商品公告和轮播图请求")
-
- # 1. 查询公告(类型为1)
- gonggao_obj = Gonggao.objects.filter(leixing=1).first()
-
- # 获取公告内容,如果没有则返回空字符串
- gonggao_content = gonggao_obj.jieshao if gonggao_obj else ""
-
- # 2. 查询轮播图(类型为1,按显示顺序排序)
- lunbo_queryset = Lunbo.objects.filter(leixing=1)
-
- # 提取轮播图URL列表
- lunbo_urls = [lunbo.tupian_url for lunbo in lunbo_queryset]
-
- # 3. 构建返回数据
- response_data = {
- "shangpingonggao": gonggao_content,
- "shangpinlunbo": lunbo_urls
- }
-
- logger.info(f"返回数据:公告长度{len(gonggao_content)},轮播图数量{len(lunbo_urls)}")
-
- # 4. 返回响应
- return Response(response_data, status=status.HTTP_200_OK)
-
- except Exception as e:
- # 记录异常
- logger.error(f"获取商品公告和轮播图时发生错误:{str(e)}")
-
- # 返回错误响应
- error_data = {
- "error": "服务器内部错误",
- "detail": "获取数据失败,请稍后重试"
- }
- return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-
-# peizhi/views.py
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status, permissions
-from rest_framework.parsers import MultiPartParser, FormParser
-from django.db import transaction
-from yonghu.models import UserMain, AdminProfile
-from .models import Tupianpeizhi, Qunpeizhi
-from dingdan.models import Lilubiao
-import json
-import os
-import uuid
-from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
-
-class AdminConfigQueryView(APIView):
- """
- 管理员获取系统配置接口
- URL: /peizhi/adpzhq
- 方法: POST
- 权限: JWT Token认证 + 管理员权限
- 参数: zhanghao (管理员账号)
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- # 获取前端传递的账号
- zhanghao = request.data.get('zhanghao')
- if not zhanghao:
- return Response({
- 'code': 1,
- 'msg': '账号不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取当前用户(UserMain实例)
- user_main = request.user
-
- # 验证账号:比较user_main的phone与前端传递的zhanghao
- if user_main.phone != zhanghao:
- return Response({
- 'code': 1,
- 'msg': '账号不匹配',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 验证用户类型是否为管理员
- if user_main.user_type != 'admin':
- return Response({
- 'code': 1,
- 'msg': '无权限',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 验证管理员扩展表是否存在
- try:
- admin_profile = user_main.admin_profile
- except AdminProfile.DoesNotExist:
- return Response({
- 'code': 1,
- 'msg': '无权限',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 查询图片配置
- rule_image = Tupianpeizhi.objects.filter(leixing=1).first()
- default_avatar = Tupianpeizhi.objects.filter(leixing=2).first()
-
- # 查询群配置
- qun_configs = Qunpeizhi.objects.all()
- qq_groups = []
- for config in qun_configs:
- qq_groups.append({
- 'peizhiid': config.peizhiid,
- 'neirong': config.neirong if config.neirong else '',
- 'qunid': config.qunid if config.qunid else '',
- 'jieshao': config.jieshao if config.jieshao else ''
- })
-
- # 查询利率配置
- platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first()
- shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='3').first()
- # 🔴【新增】打手提现抽成利率(字符5)
- dashou_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='5').first()
- # 🔴【新增】管事提现抽成利率(字符6)
- guanshi_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='6').first()
-
- # 构建返回数据
- data = {
- 'images': {
- 'rule_image': rule_image.tupian if rule_image and rule_image.tupian else '',
- 'default_avatar': default_avatar.tupian if default_avatar and default_avatar.tupian else ''
- },
- 'qq_groups': qq_groups,
- 'rates': {
- 'platform_rate': float(platform_rate_obj.lilu) if platform_rate_obj and platform_rate_obj.lilu else 0.0,
- 'shop_rate': float(shop_rate_obj.lilu) if shop_rate_obj and shop_rate_obj.lilu else 0.0,
- # 🔴【新增】打手提现抽成利率
- 'dashou_withdraw_rate': float(
- dashou_withdraw_obj.lilu) if dashou_withdraw_obj and dashou_withdraw_obj.lilu else 0.0,
- # 🔴【新增】管事提现抽成利率
- 'guanshi_withdraw_rate': float(
- guanshi_withdraw_obj.lilu) if guanshi_withdraw_obj and guanshi_withdraw_obj.lilu else 0.0
-
- }
- }
-
- return Response({
- 'code': 0,
- 'msg': 'success',
- 'data': data
- })
-
- except Exception as e:
- return Response({
- 'code': 99,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class AdminUploadImageView(APIView):
- """
- 管理员上传配置图片接口
- URL: /peizhi/adxgtp
- 方法: POST
- 权限: JWT Token认证 + 管理员权限
- 参数: zhanghao, leixing (1:打手规则图片, 2:默认头像图片), image (文件)
- """
- permission_classes = [permissions.IsAuthenticated]
- parser_classes = (MultiPartParser, FormParser)
-
- def post(self, request):
- try:
- zhanghao = request.data.get('zhanghao')
- leixing = request.data.get('leixing')
- image_file = request.FILES.get('image')
-
- if not zhanghao:
- return Response({
- 'code': 1,
- 'msg': '账号不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not leixing:
- return Response({
- 'code': 2,
- 'msg': '图片类型不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not image_file:
- return Response({
- 'code': 3,
- 'msg': '请选择图片文件',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证用户权限
- user_main = request.user
- if user_main.phone != zhanghao:
- return Response({
- 'code': 1,
- 'msg': '账号不匹配',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- if user_main.user_type != 'admin':
- return Response({
- 'code': 1,
- 'msg': '无权限',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- admin_profile = user_main.admin_profile
- except AdminProfile.DoesNotExist:
- return Response({
- 'code': 1,
- 'msg': '无权限',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 验证图片类型
- if leixing not in ['1', '2']:
- return Response({
- 'code': 4,
- 'msg': '图片类型错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证图片文件
- is_valid, error_msg = validate_image(image_file)
- if not is_valid:
- return Response({
- 'code': 5,
- 'msg': f'图片验证失败: {error_msg}',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 根据类型确定固定相对URL和文件名
- if leixing == '1':
- # 打手规则图片
- fixed_relative_url = 'a_long/dashouguize.jpg'
- oss_file_name = 'dashouguize.jpg'
- else:
- # 默认头像图片
- fixed_relative_url = 'a_long/morentouxiang.jpg'
- oss_file_name = 'morentouxiang.jpg'
-
- # 构建OSS文件路径
- oss_file_path = fixed_relative_url # a_long/dashouguize.jpg 或 a_long/morentouxiang.jpg
-
- # 上传到OSS(覆盖原有文件)
- image_file.seek(0) # 重置文件指针
- full_url = upload_to_oss(image_file, oss_file_path)
-
- if not full_url:
- return Response({
- 'code': 6,
- 'msg': '图片上传到存储桶失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 更新数据库
- with transaction.atomic():
- # 获取或创建图片配置记录
- tupian_peizhi, created = Tupianpeizhi.objects.get_or_create(leixing=int(leixing))
- tupian_peizhi.tupian = fixed_relative_url
- tupian_peizhi.save()
-
- return Response({
- 'code': 0,
- 'msg': '图片上传成功',
- 'data': {
- 'image_url': fixed_relative_url
- }
- })
-
- except Exception as e:
- return Response({
- 'code': 99,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class AdminUpdateConfigView(APIView):
- """
- 管理员更新系统配置接口(QQ群、利率)- 已修复
- URL: /peizhi/adupdate
- 方法: POST
- 权限: JWT Token认证 + 管理员权限
- 参数:
- - zhanghao: 管理员账号
- - qq_groups: [{"peizhiid": 1, "neirong": "", "qunid": "", "jieshao": ""}, ...] (可选)
- - rates: {"platform_rate": 0.1, "shop_rate": 0.2} (可选)
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- zhanghao = request.data.get('zhanghao')
- if not zhanghao:
- return Response({
- 'code': 1,
- 'msg': '账号不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证用户权限
- user_main = request.user
- if user_main.phone != zhanghao:
- return Response({
- 'code': 1,
- 'msg': '账号不匹配',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- if user_main.user_type != 'admin':
- return Response({
- 'code': 1,
- 'msg': '无权限',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- admin_profile = user_main.admin_profile
- except AdminProfile.DoesNotExist:
- return Response({
- 'code': 1,
- 'msg': '无权限',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- response_data = {}
-
- # 🔴 修复:更新QQ群配置 - 确保根据peizhiid正确更新对应的记录
- qq_groups = request.data.get('qq_groups')
- if qq_groups and isinstance(qq_groups, list):
- updated_groups = []
- for group_data in qq_groups:
- peizhiid = group_data.get('peizhiid')
- if not peizhiid:
- continue
-
- # 🔴 修复:使用filter()确保查询到正确的记录
- qun_configs = Qunpeizhi.objects.filter(peizhiid=peizhiid)
-
- if qun_configs.exists():
- # 如果存在多个相同peizhiid的记录,取第一个(应该只有一个)
- qun_config = qun_configs.first()
-
- # 更新字段
- neirong = group_data.get('neirong')
- qunid = group_data.get('qunid')
- jieshao = group_data.get('jieshao')
-
- if neirong is not None:
- qun_config.neirong = neirong or ''
- if qunid is not None:
- qun_config.qunid = qunid or ''
- if jieshao is not None:
- qun_config.jieshao = jieshao or ''
-
- qun_config.save()
- else:
- # 如果不存在,创建新的(使用提供的peizhiid)
- qun_config = Qunpeizhi.objects.create(
- peizhiid=peizhiid,
- neirong=group_data.get('neirong', '') or '',
- qunid=group_data.get('qunid', '') or '',
- jieshao=group_data.get('jieshao', '') or ''
- )
-
- updated_groups.append({
- 'peizhiid': qun_config.peizhiid,
- 'neirong': qun_config.neirong,
- 'qunid': qun_config.qunid,
- 'jieshao': qun_config.jieshao
- })
-
- if updated_groups:
- response_data['qq_groups'] = updated_groups
-
- # 🔴 修复:更新利率配置 - 确保更新现有记录而不是创建新记录
- rates = request.data.get('rates')
- if rates and isinstance(rates, dict):
- updated_rates = {}
-
- # 平台发单利率(类型1,fadanpingtai='1')
- platform_rate = rates.get('platform_rate')
- if platform_rate is not None:
- try:
- platform_rate_float = float(platform_rate)
- if 0 <= platform_rate_float <= 1:
- # 🔴 修复:查询现有记录,如果不存在则创建
- platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first()
-
- if platform_rate_obj:
- # 更新现有记录
- platform_rate_obj.lilu = platform_rate_float
- platform_rate_obj.save()
- else:
- # 创建新记录
- platform_rate_obj = Lilubiao.objects.create(
- lilu=platform_rate_float,
- fadanpingtai='1'
- )
-
- updated_rates['platform_rate'] = platform_rate_float
- else:
- return Response({
- 'code': 3,
- 'msg': '平台发单利率必须在0-1之间',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- except ValueError:
- return Response({
- 'code': 4,
- 'msg': '平台发单利率格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 商家发单利率(类型2,fadanpingtai='2')
- shop_rate = rates.get('shop_rate')
- if shop_rate is not None:
- try:
- shop_rate_float = float(shop_rate)
- if 0 <= shop_rate_float <= 1:
- # 🔴 修复:查询现有记录,如果不存在则创建
- shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='3').first()
-
- if shop_rate_obj:
- # 更新现有记录
- shop_rate_obj.lilu = shop_rate_float
- shop_rate_obj.save()
- else:
- # 创建新记录
- shop_rate_obj = Lilubiao.objects.create(
- lilu=shop_rate_float,
- fadanpingtai='3'
- )
-
- updated_rates['shop_rate'] = shop_rate_float
- else:
- return Response({
- 'code': 5,
- 'msg': '商家发单利率必须在0-1之间',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- except ValueError:
- return Response({
- 'code': 6,
- 'msg': '商家发单利率格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
-
-
-
- # 🔴【新增】打手提现抽成利率(类型5,fadanpingtai='5')
- dashou_withdraw_rate = rates.get('dashou_withdraw_rate')
- if dashou_withdraw_rate is not None:
- try:
- dashou_withdraw_rate_float = float(dashou_withdraw_rate)
- if 0 <= dashou_withdraw_rate_float <= 1:
- # 查询现有记录,如果不存在则创建
- dashou_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='5').first()
-
- if dashou_withdraw_obj:
- # 更新现有记录
- dashou_withdraw_obj.lilu = dashou_withdraw_rate_float
- dashou_withdraw_obj.save()
- else:
- # 创建新记录
- dashou_withdraw_obj = Lilubiao.objects.create(
- lilu=dashou_withdraw_rate_float,
- fadanpingtai='5'
- )
-
- updated_rates['dashou_withdraw_rate'] = dashou_withdraw_rate_float
- else:
- return Response({
- 'code': 7, # 注意:这里需要调整错误码,不要重复
- 'msg': '打手提现抽成利率必须在0-1之间',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- except ValueError:
- return Response({
- 'code': 8,
- 'msg': '打手提现抽成利率格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 🔴【新增】管事提现抽成利率(类型6,fadanpingtai='6')
- guanshi_withdraw_rate = rates.get('guanshi_withdraw_rate')
- if guanshi_withdraw_rate is not None:
- try:
- guanshi_withdraw_rate_float = float(guanshi_withdraw_rate)
- if 0 <= guanshi_withdraw_rate_float <= 1:
- # 查询现有记录,如果不存在则创建
- guanshi_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='6').first()
-
- if guanshi_withdraw_obj:
- # 更新现有记录
- guanshi_withdraw_obj.lilu = guanshi_withdraw_rate_float
- guanshi_withdraw_obj.save()
- else:
- # 创建新记录
- guanshi_withdraw_obj = Lilubiao.objects.create(
- lilu=guanshi_withdraw_rate_float,
- fadanpingtai='6'
- )
-
- updated_rates['guanshi_withdraw_rate'] = guanshi_withdraw_rate_float
- else:
- return Response({
- 'code': 9,
- 'msg': '管事提现抽成利率必须在0-1之间',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- except ValueError:
- return Response({
- 'code': 10,
- 'msg': '管事提现抽成利率格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if updated_rates:
- response_data['rates'] = updated_rates
-
- # 如果没有任何更新
- if not response_data:
- return Response({
- 'code': 7,
- 'msg': '没有需要更新的数据',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- return Response({
- 'code': 0,
- 'msg': '更新成功',
- 'data': response_data
- })
-
- except Exception as e:
- return Response({
- 'code': 99,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-'''class AdminUpdateConfigView(APIView):
- """
- 管理员更新系统配置接口(QQ群、利率)
- URL: /peizhi/adupdate
- 方法: POST
- 权限: JWT Token认证 + 管理员权限
- 参数:
- - zhanghao: 管理员账号
- - qq_groups: [{"peizhiid": 1, "neirong": "", "qunid": "", "jieshao": ""}, ...] (可选)
- - rates: {"platform_rate": 0.1, "shop_rate": 0.2} (可选)
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- zhanghao = request.data.get('zhanghao')
- if not zhanghao:
- return Response({
- 'code': 1,
- 'msg': '账号不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证用户权限
- user_main = request.user
- if user_main.phone != zhanghao:
- return Response({
- 'code': 1,
- 'msg': '账号不匹配',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- if user_main.user_type != 'admin':
- return Response({
- 'code': 1,
- 'msg': '无权限',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- admin_profile = user_main.admin_profile
- except AdminProfile.DoesNotExist:
- return Response({
- 'code': 1,
- 'msg': '无权限',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- response_data = {}
-
- # 更新QQ群配置
- qq_groups = request.data.get('qq_groups')
- if qq_groups and isinstance(qq_groups, list):
- updated_groups = []
- for group_data in qq_groups:
- peizhiid = group_data.get('peizhiid')
- if not peizhiid:
- continue
-
- # 查找配置
- qun_config = Qunpeizhi.objects.filter(peizhiid=peizhiid).first()
- if not qun_config:
- # 如果不存在,创建新的
- qun_config = Qunpeizhi(peizhiid=peizhiid)
-
- # 更新字段(peizhiid不可改)
- qun_config.neirong = group_data.get('neirong', qun_config.neirong) or ''
- qun_config.qunid = group_data.get('qunid', qun_config.qunid) or ''
- qun_config.jieshao = group_data.get('jieshao', qun_config.jieshao) or ''
- qun_config.save()
-
- updated_groups.append({
- 'peizhiid': qun_config.peizhiid,
- 'neirong': qun_config.neirong,
- 'qunid': qun_config.qunid,
- 'jieshao': qun_config.jieshao
- })
-
- if updated_groups:
- response_data['qq_groups'] = updated_groups
-
- # 更新利率配置
- rates = request.data.get('rates')
- if rates and isinstance(rates, dict):
- updated_rates = {}
-
- # 平台发单利率(类型1)
- platform_rate = rates.get('platform_rate')
- if platform_rate is not None:
- try:
- platform_rate_float = float(platform_rate)
- if 0 <= platform_rate_float <= 1:
- platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first()
- if not platform_rate_obj:
- platform_rate_obj = Lilubiao.objects.create(
- lilu=platform_rate_float,
- fadanpingtai='1'
- )
- else:
- platform_rate_obj.lilu = platform_rate_float
- platform_rate_obj.save()
-
- updated_rates['platform_rate'] = platform_rate_float
- else:
- return Response({
- 'code': 3,
- 'msg': '平台发单利率必须在0-1之间',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- except ValueError:
- return Response({
- 'code': 4,
- 'msg': '平台发单利率格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 商家发单利率(类型2)
- shop_rate = rates.get('shop_rate')
- if shop_rate is not None:
- try:
- shop_rate_float = float(shop_rate)
- if 0 <= shop_rate_float <= 1:
- shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='2').first()
- if not shop_rate_obj:
- shop_rate_obj = Lilubiao.objects.create(
- lilu=shop_rate_float,
- fadanpingtai='3'
- )
- else:
- shop_rate_obj.lilu = shop_rate_float
- shop_rate_obj.save()
-
- updated_rates['shop_rate'] = shop_rate_float
- else:
- return Response({
- 'code': 5,
- 'msg': '商家发单利率必须在0-1之间',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- except ValueError:
- return Response({
- 'code': 6,
- 'msg': '商家发单利率格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if updated_rates:
- response_data['rates'] = updated_rates
-
- # 如果没有任何更新
- if not response_data:
- return Response({
- 'code': 7,
- 'msg': '没有需要更新的数据',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- return Response({
- 'code': 0,
- 'msg': '更新成功',
- 'data': response_data
- })
-
- except Exception as e:
- return Response({
- 'code': 99,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)'''
-
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from django.db.models import Q
-from decimal import Decimal
-import logging
-
-from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia, UserGuanshi
-
-logger = logging.getLogger(__name__)
-
-
-class DashouPaihangView(APIView):
- """
- 排行榜接口
- 路径:/peizhi/dsph
- 方法:POST
- 认证:JWT Token认证
-
- 请求参数:
- - leixing: 1=今日收入,2=今月收入
- - yonghu_leixing: 1=打手,2=管事,3=商家
-
- 返回数据:
- - code: 0=成功,其他=失败
- - msg: 提示信息
- - data: {
- xianshi: True/False, # 是否展示真实数据
- paihang_list: [ # 排行榜列表
- {
- uid: '用户ID',
- touxiang: '头像相对URL',
- nicheng: '用户昵称',
- jine: 收入金额,
- mingci: 排名
- },
- ...
- ]
- }
- """
-
- # =============== 可配置参数 ===============
- # 每次查询返回的最大记录数(可修改)
- ZUI_DAO_CHA_XUN_SHU_LIANG = 50
-
- # 最少需要多少人才能展示(可修改)
- ZUI_SHAO_REN_SHU_YAO_QIU = 1
-
- # 第一名最低金额要求(单位:元)(可修改)
- DI_YI_MING_ZUI_DI_JIN_E = Decimal('10.00')
-
- # 管事第一名最低数量要求(可修改)
- DI_YI_MING_ZUI_DI_SHU_LIANG = 1
- # =============== 可配置参数结束 ===============
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 获取请求参数 - 修复参数类型问题
- leixing_str = request.data.get('leixing', '1')
- yonghu_leixing_str = request.data.get('yonghu_leixing', '1')
-
- # 转换为整数
- try:
- leixing = int(leixing_str)
- except (ValueError, TypeError):
- leixing = 1
-
- try:
- yonghu_leixing = int(yonghu_leixing_str)
- except (ValueError, TypeError):
- yonghu_leixing = 1
-
- # 参数验证
- if leixing not in [1, 2]:
- return Response({
- 'code': 400,
- 'msg': '参数错误:leixing必须为1(今日)或2(今月)',
- 'data': {'xianshi': False}
- })
-
- if yonghu_leixing not in [1, 2, 3]:
- return Response({
- 'code': 400,
- 'msg': '参数错误:yonghu_leixing必须为1(打手)、2(管事)或3(商家)',
- 'data': {'xianshi': False}
- })
-
- # 根据用户类型调用不同的查询方法
- if yonghu_leixing == 1:
- # 打手排行榜
- paihang_list, xianshi_flag = self.chaxun_dashou_paihang(leixing)
- elif yonghu_leixing == 2:
- # 管事排行榜
- paihang_list, xianshi_flag = self.chaxun_guanshi_paihang(leixing)
- else:
- # 商家排行榜
- paihang_list, xianshi_flag = self.chaxun_shangjia_paihang(leixing)
-
- # 返回结果
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': {
- 'xianshi': xianshi_flag,
- 'paihang_list': paihang_list
- }
- })
-
- except Exception as e:
- logger.error(f"排行榜查询失败: {str(e)}", exc_info=True)
- return Response({
- 'code': 500,
- 'msg': '服务器内部错误',
- 'data': {'xianshi': False}
- })
-
- def chaxun_dashou_paihang(self, leixing):
- """
- 查询打手排行榜
- :param leixing: 1=今日收入,2=今月收入
- :return: (排行榜列表, 是否展示标志)
- """
- # 确定查询的字段
- if leixing == 1:
- # 今日收入
- shouyi_ziduan = 'jinrishouyi'
- else:
- # 今月收入
- shouyi_ziduan = 'jinyueshouyi'
-
- # 查询条件:收入大于0,打手账号状态正常(1=正常)
- # 注意:打手表中的zhanghaozhuangtai字段
- chaxun_tiaojian = Q(**{f'{shouyi_ziduan}__gt': 0}) & Q(zhanghaozhuangtai=1)
-
- # 查询打手扩展表,并关联用户主表
- dashou_list = UserDashou.objects.select_related('user').filter(
- chaxun_tiaojian
- ).order_by(f'-{shouyi_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
-
- # 检查是否满足展示条件
- if len(dashou_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
- # 人数不足,不展示
- return [], False
-
- # 检查第一名金额是否达到最低要求
- if leixing == 1:
- di_yi_ming_jine = dashou_list[0].jinrishouyi
- else:
- di_yi_ming_jine = dashou_list[0].jinyueshouyi
-
- if di_yi_ming_jine < self.DI_YI_MING_ZUI_DI_JIN_E:
- # 第一名金额不足,不展示
- return [], False
-
- # 构建返回数据
- paihang_list = []
- for index, dashou in enumerate(dashou_list):
- # 获取用户主表信息
- user_main = dashou.user
-
- # 打手的昵称直接使用打手扩展表的nicheng字段
- dashou_nicheng = dashou.nicheng or '未设置昵称'
-
- # 获取头像(相对URL)
- touxiang_url = user_main.avatar or ''
-
- # 获取收入金额
- if leixing == 1:
- shouyi_jine = dashou.jinrishouyi
- else:
- shouyi_jine = dashou.jinyueshouyi
-
- paihang_list.append({
- 'uid': user_main.yonghuid,
- 'touxiang': touxiang_url,
- 'nicheng': dashou_nicheng,
- 'jine': float(shouyi_jine), # 转换为float方便前端处理
- 'mingci': index + 1
- })
-
- return paihang_list, True
-
- def chaxun_guanshi_paihang(self, leixing):
- """
- 查询管事排行榜
- :param leixing: 1=今日,2=今月
- :return: (排行榜列表, 是否展示标志)
- """
- # 确定查询的字段
- if leixing == 1:
- # 今日充值打手数量
- chaxun_ziduan = 'jinrichongzhi'
- else:
- # 今月充值打手数量
- chaxun_ziduan = 'jinyuechongzhi'
-
- # 查询条件:数量大于0,管事状态正常(状态1为正常)
- # 注意:管手表中的zhuangtai字段
- chaxun_tiaojian = Q(**{f'{chaxun_ziduan}__gt': 0}) & Q(zhuangtai=1)
-
- # 查询管事扩展表,并关联用户主表
- guanshi_list = UserGuanshi.objects.select_related('user').filter(
- chaxun_tiaojian
- ).order_by(f'-{chaxun_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
-
- # 检查是否满足展示条件
- if len(guanshi_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
- # 人数不足,不展示
- return [], False
-
- # 检查第一名数量是否达到最低要求
- if leixing == 1:
- di_yi_ming_shuliang = guanshi_list[0].jinrichongzhi
- else:
- di_yi_ming_shuliang = guanshi_list[0].jinyuechongzhi
-
- if di_yi_ming_shuliang < self.DI_YI_MING_ZUI_DI_SHU_LIANG:
- # 第一名数量不足,不展示
- return [], False
-
- # 构建返回数据
- paihang_list = []
- for index, guanshi in enumerate(guanshi_list):
- # 获取用户主表信息
- user_main = guanshi.user
-
- # 管事的昵称需要查询老板扩展表
- boss_profile = UserBoss.objects.filter(user=user_main).first()
- guanshi_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
-
- # 获取头像(相对URL)
- touxiang_url = user_main.avatar or ''
-
- # 获取数量
- if leixing == 1:
- chongzhi_shuliang = guanshi.jinrichongzhi
- else:
- chongzhi_shuliang = guanshi.jinyuechongzhi
-
- paihang_list.append({
- 'uid': user_main.yonghuid,
- 'touxiang': touxiang_url,
- 'nicheng': guanshi_nicheng,
- 'jine': float(chongzhi_shuliang), # 这里jine字段实际存储的是数量
- 'mingci': index + 1
- })
-
- return paihang_list, True
-
- def chaxun_shangjia_paihang(self, leixing):
- """
- 查询商家排行榜
- :param leixing: 1=今日流水,2=今月流水
- :return: (排行榜列表, 是否展示标志)
- """
- # 确定查询的字段
- if leixing == 1:
- # 今日流水
- liushui_ziduan = 'jinriliushui'
- else:
- # 今月流水
- liushui_ziduan = 'jinyueliushui'
-
- # 查询条件:流水大于0,商家状态正常(状态1为正常)
- # 注意:商家表中的zhuangtai字段
- chaxun_tiaojian = Q(**{f'{liushui_ziduan}__gt': 0}) & Q(zhuangtai=1)
-
- # 查询商家扩展表,并关联用户主表
- shangjia_list = UserShangjia.objects.select_related('user').filter(
- chaxun_tiaojian
- ).order_by(f'-{liushui_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
-
- # 检查是否满足展示条件
- if len(shangjia_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
- # 人数不足,不展示
- return [], False
-
- # 检查第一名金额是否达到最低要求
- if leixing == 1:
- di_yi_ming_liushui = shangjia_list[0].jinriliushui
- else:
- di_yi_ming_liushui = shangjia_list[0].jinyueliushui
-
- if di_yi_ming_liushui < self.DI_YI_MING_ZUI_DI_JIN_E:
- # 第一名流水不足,不展示
- return [], False
-
- # 构建返回数据
- paihang_list = []
- for index, shangjia in enumerate(shangjia_list):
- # 获取用户主表信息
- user_main = shangjia.user
-
- # 商家的昵称需要查询老板扩展表
- boss_profile = UserBoss.objects.filter(user=user_main).first()
- shangjia_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
-
- # 获取头像(相对URL)
- touxiang_url = user_main.avatar or ''
-
- # 获取流水金额
- if leixing == 1:
- liushui_jine = shangjia.jinriliushui
- else:
- liushui_jine = shangjia.jinyueliushui
-
- paihang_list.append({
- 'uid': user_main.yonghuid,
- 'touxiang': touxiang_url,
- 'nicheng': shangjia_nicheng,
- 'jine': float(liushui_jine),
- 'mingci': index + 1
- })
-
- return paihang_list, True
-
-
-
-
-
-
-
-
-
-
-# views.py
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from django.db import connection
-from django.db.models import Q
-import time
-
-
-class GetAdminZhanghaoView(APIView):
- """
- 获取管理员账号列表接口
- 路径:/peizhi/adzhhq
- 方法:POST
- 权限:JWT Token + 管理员验证
- 参数:
- - zhanghao: 当前登录的管理员账号(字符串)
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 🔥 1. 身份验证(100%按表模型)
- user = request.user
-
- # 获取当前用户的手机号(CharField,字符串类型)
- current_phone = getattr(user, 'phone', '')
- if not current_phone:
- return Response({
- 'code': 401,
- 'message': '用户手机号信息不完整',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 检查前端传递的账号是否匹配(都是字符串)
- frontend_zhanghao = request.data.get('zhanghao', '').strip()
- if frontend_zhanghao != current_phone:
- return Response({
- 'code': 403,
- 'message': '账号不匹配,无权限访问',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 🔥 2. 验证是否为管理员(user_type是CharField,值是'admin'字符串)
- user_type = getattr(user, 'user_type', '')
- if user_type != 'admin':
- # 检查是否有管理员扩展表
- if not hasattr(user, 'admin_profile'):
- return Response({
- 'code': 403,
- 'message': '无管理员权限',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 🔥 3. 查询管理员账号列表(全部按字符串查询)
- with connection.cursor() as cursor:
- # 🔥 查询所有管理员账号(user_type='admin' 字符串)
- # 🔥 phone和yonghuid都是CharField,xiugaiid也是CharField
- sql = """
- SELECT
- um.id,
- um.phone,
- um.yonghuid,
- um.user_type,
- COALESCE(COUNT(xjl.id), 0) as jilushuliang
- FROM user_main um
- LEFT JOIN xiugaijilu xjl ON um.phone = xjl.xiugaiid
- WHERE um.user_type = 'admin'
- GROUP BY um.id, um.phone, um.yonghuid, um.user_type
- ORDER BY um.create_time DESC
- """
-
- cursor.execute(sql)
- results = cursor.fetchall()
-
- # 🔥 4. 处理查询结果(全部保持字符串类型)
- zhanghao_list = []
- for row in results:
- zhanghao_list.append({
- #'id': row[0],
- 'id': str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
- 'zhanghao':str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
- 'yonghuid': str(row[2]) if row[2] else '', # 🔥 保持字符串
- 'user_type': str(row[3]) if row[3] else '', # 🔥 保持字符串
- 'jilushuliang': int(row[4]) # 只有这个是数字
- })
-
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': zhanghao_list
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- print(f"获取管理员账号列表失败: {str(e)}")
- import traceback
- traceback.print_exc()
- return Response({
- 'code': 500,
- 'message': '系统错误',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class GetXiugaiJiluView(APIView):
- """
- 获取修改记录列表接口
- 路径:/peizhi/adhqxg
- 方法:POST
- 权限:JWT Token + 管理员验证
- 参数:
- - zhanghao: 当前登录的管理员账号(字符串)
- - xiugaizhe_zhanghaoid: 选中的管理员账号ID(字符串,对应phone或yonghuid)
- - page: 页码(整数)
- - pagesize: 每页条数(整数)
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 🔥 1. 身份验证(100%按表模型)
- user = request.user
-
- # 获取当前用户的手机号(CharField,字符串)
- current_phone = getattr(user, 'phone', '')
- if not current_phone:
- return Response({
- 'code': 401,
- 'message': '用户手机号信息不完整',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 检查前端传递的账号是否匹配(字符串比较)
- frontend_zhanghao = request.data.get('zhanghao', '')
- if not isinstance(frontend_zhanghao, str):
- frontend_zhanghao = str(frontend_zhanghao)
- frontend_zhanghao = frontend_zhanghao.strip()
-
- if frontend_zhanghao != current_phone:
- return Response({
- 'code': 403,
- 'message': '账号不匹配,无权限访问',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 🔥 2. 验证是否为管理员(user_type='admin'字符串)
- user_type = getattr(user, 'user_type', '')
- if user_type != 'admin':
- if not hasattr(user, 'admin_profile'):
- return Response({
- 'code': 403,
- 'message': '无管理员权限',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 🔥 3. 获取参数(100%按实际类型)
- # 🔥 xiugaizhe_zhanghaoid 是字符串(对应xiugaiid字段,CharField)
- xiugaizhe_zhanghaoid_param = request.data.get('xiugaizhe_zhanghaoid', '')
-
-
- # 🔥 强制转为字符串(因为数据库是CharField)
- if xiugaizhe_zhanghaoid_param is None:
- xiugaizhe_zhanghaoid = ''
- else:
- # 无论如何都转为字符串
- xiugaizhe_zhanghaoid = str(xiugaizhe_zhanghaoid_param).strip()
-
-
- # 🔥 分页参数转为整数
- try:
- page = int(request.data.get('page', 1))
- except:
- page = 1
-
- try:
- pagesize = int(request.data.get('pagesize', 10))
- except:
- pagesize = 10
-
- # 🔥 4. 参数验证
- if not xiugaizhe_zhanghaoid:
- return Response({
- 'code': 400,
- 'message': '修改者账号ID不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if page < 1:
- page = 1
- if pagesize < 1 or pagesize > 50:
- pagesize = 10
-
- # 🔥 5. 高效查询(100%按表字段类型)
- with connection.cursor() as cursor:
- # 🔥 5.1 查询总记录数(xiugaiid是CharField,用字符串查询)
- count_sql = """
- SELECT COUNT(*)
- FROM xiugaijilu
- WHERE xiugaiid = %s
- """
- cursor.execute(count_sql, [xiugaizhe_zhanghaoid])
- total_count = cursor.fetchone()[0]
-
- # 🔥 5.2 计算分页
- offset = (page - 1) * pagesize
-
- # 🔥 5.3 查询当前页数据(使用参数化查询,防止SQL注入)
- data_sql = """
- SELECT
- xjl.id,
- xjl.yonghuid, -- CharField
- xjl.xiugaiid, -- CharField
- xjl.leixing, -- IntegerField
- xjl.xiugaitijiao, -- DecimalField
- xjl.xiugaitijiaoq, -- DecimalField
- xjl.jifen, -- IntegerField
- xjl.yjifen, -- IntegerField
- xjl.yajin, -- DecimalField
- xjl.yyajin, -- DecimalField
- xjl.shangjiayue, -- DecimalField
- xjl.shangjiayueq, -- DecimalField
- xjl.guanshiyue, -- DecimalField
- xjl.guanshiyueq, -- DecimalField
- xjl.huiyuants, -- IntegerField
- xjl.huiyuan_id, -- CharField
- xjl.qitashuoming, -- TextField
- xjl.create_time, -- DateTimeField
- um.avatar -- CharField
- FROM xiugaijilu xjl
- LEFT JOIN user_main um ON xjl.yonghuid = um.yonghuid
- WHERE xjl.xiugaiid = %s
- ORDER BY xjl.create_time DESC
- LIMIT %s OFFSET %s
- """
-
- cursor.execute(data_sql, [xiugaizhe_zhanghaoid, pagesize, offset])
- records = cursor.fetchall()
-
- # 🔥 6. 处理查询结果(严格按照字段类型处理)
- jilu_list = []
- for record in records:
- # 获取修改者账号信息(都是字符串)
- xiugaizhe_zhanghao = ''
- with connection.cursor() as cursor2:
- # 🔥 phone和yonghuid都是CharField,用字符串查询
- cursor2.execute(
- "SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1",
- [xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid]
- )
- xiugaizhe_info = cursor2.fetchone()
- if xiugaizhe_info:
- # 🔥 都是字符串
- xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \
- xiugaizhe_info[1] else xiugaizhe_zhanghaoid
-
- # 获取会员名称(如果有)
- huiyuan_mingzi = ''
- if record[15]: # huiyuan_id是CharField
- with connection.cursor() as cursor3:
- cursor3.execute(
- "SELECT jieshao FROM huiyuan WHERE huiyuan_id = %s LIMIT 1",
- [str(record[15])] # 🔥 转为字符串查询
- )
- huiyuan_info = cursor3.fetchone()
- if huiyuan_info:
- huiyuan_mingzi = str(huiyuan_info[0]) if huiyuan_info[0] else ''
-
- # 🔥 严格按照前端字段名映射,保持正确类型
- jilu_data = {
- # 基础字段(字符串)
- 'id': record[0],
- 'beixiugai_yonghuid': str(record[1]) if record[1] else '', # yonghuid
- 'xiugaizhe_id': str(record[2]) if record[2] else '', # xiugaiid
- 'xiugaizhe_zhanghao': xiugaizhe_zhanghao, # 字符串
- 'beixiugai_yonghuleixing': record[3], # leixing是整数
-
- # 打手字段(Decimal转float,Integer转int)
- 'xiugaihou_dashoutixianjine': float(record[4]) if record[4] is not None else 0.00,
- 'xiugaiqian_dashoutixianjine': float(record[5]) if record[5] is not None else 0.00,
- 'xiugaihou_dashoujifen': int(record[6]) if record[6] is not None else 0,
- 'xiugaiqian_dashoujifen': int(record[7]) if record[7] is not None else 0,
- 'xiugaihou_dashouyajin': float(record[8]) if record[8] is not None else 0.00,
- 'xiugaiqian_dashouyajin': float(record[9]) if record[9] is not None else 0.00,
-
- # 商家字段
- 'xiugaihou_shangjiayue': float(record[10]) if record[10] is not None else 0.00,
- 'xiugaiqian_shangjiayue': float(record[11]) if record[11] is not None else 0.00,
-
- # 管事字段
- 'xiugaihou_guanshiyue': float(record[12]) if record[12] is not None else 0.00,
- 'xiugaiqian_guanshiyue': float(record[13]) if record[13] is not None else 0.00,
-
- # 会员字段
- 'tianjia_huiyuantianshu': int(record[14]) if record[14] is not None else 0,
- 'huiyuan_id': str(record[15]) if record[15] else '', # 字符串
- 'huiyuan_mingzi': huiyuan_mingzi, # 字符串
-
- # 其他字段
- 'qita_shuoming': str(record[16]) if record[16] else '', # 字符串
- 'xiugai_time': record[17].strftime('%Y-%m-%d %H:%M:%S') if record[17] else None,
- 'touxiang': str(record[18]) if record[18] else '' # avatar是字符串
- }
-
- jilu_list.append(jilu_data)
-
- # 🔥 7. 分页控制
- queried_records = len(jilu_list)
- has_more = False
-
- if queried_records == pagesize:
- total_queried = (page - 1) * pagesize + queried_records
- has_more = total_queried < total_count
-
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': {
- 'list': jilu_list,
- 'zongshuliang': total_count,
- 'dangqian_page': page,
- 'page_size': pagesize,
- 'has_more': has_more,
- 'shijichaxundediaoshuliang': queried_records
- }
- }, status=status.HTTP_200_OK)
-
- except ValueError as e:
- print(f"参数错误: {str(e)}")
- return Response({
- 'code': 400,
- 'message': '参数错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- except Exception as e:
- print(f"获取修改记录失败: {str(e)}")
- import traceback
- traceback.print_exc()
- return Response({
- 'code': 500,
- 'message': '系统错误',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-
-
-
-
-
-
-
-# views/shangjia_moban.py
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from django.db import transaction
-from decimal import Decimal
-import re
-
-from .models import ShangjiaMoban
-from shangpin.models import ShangpinLeixing # 假设商品模型路径
-
-
-'''class ShangjiaTianjiaMobanView(APIView):
- """
- 商家添加订单模板接口
- POST /peizhi/sjtjddmb
- 权限: JWT认证用户
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 获取用户ID
- user = request.user
- yonghu_id = user.yonghuid
-
- # 2. 验证并获取请求数据
- leixing_id = request.data.get('shangpinTypeId')
- jieshao = request.data.get('jieshao', '').strip()
- jiage = request.data.get('jiage', '')
-
- # 3. 验证必填字段
- if not all([leixing_id, jieshao, jiage]):
- return Response({
- 'code': 400,
- 'msg': '缺少必填字段',
- 'data': {}
- }, status=400)
-
- # 4. 验证价格格式
- try:
- jiage_decimal = Decimal(jiage)
- if jiage_decimal <= 0 or jiage_decimal > 10000:
- return Response({
- 'code': 400,
- 'msg': '价格必须在0.1-10000元之间',
- 'data': {}
- }, status=400)
- except (ValueError, TypeError):
- return Response({
- 'code': 400,
- 'msg': '价格格式错误',
- 'data': {}
- }, status=400)
-
- # 5. 验证介绍长度
- if len(jieshao) > 100:
- return Response({
- 'code': 400,
- 'msg': '订单介绍不能超过100字',
- 'data': {}
- }, status=400)
-
- # 6. 验证商品类型是否存在且可用
- try:
- # 使用select_related优化查询,只获取需要的字段
- shangpin_obj = ShangpinLeixing.objects.filter(
- id=leixing_id,
- yaoqiuleixing=1 # 假设1为正常状态
- ).only('id', 'yaoqiuleixing').first()
-
- if not shangpin_obj:
- return Response({
- 'code': 404,
- 'msg': '商品类型不存在或已禁用',
- 'data': {}
- }, status=404)
-
- # 检查抢单要求类型
- if shangpin_obj.yaoqiuleixing != 1:
- return Response({
- 'code': 400,
- 'msg': '该商品类型不支持创建模板',
- 'data': {}
- }, status=400)
-
- except ShangpinLeixing.DoesNotExist:
- return Response({
- 'code': 404,
- 'msg': '商品类型不存在',
- 'data': {}
- }, status=404)
-
- # 7. 创建模板(使用事务保证数据一致性)
- with transaction.atomic():
- # 检查是否已存在相同介绍(根据unique_together约束)
- existing_moban = ShangjiaMoban.objects.filter(
- yonghu_id=yonghu_id,
- leixing_id=leixing_id,
- moban_jieshao=jieshao,
- #zhuangtai=1 # 只检查正常状态的模板
- ).exists()
-
- if existing_moban:
- return Response({
- 'code': 400,
- 'msg': '已存在相同介绍的模板',
- 'data': {}
- }, status=400)
-
- # 创建新模板
- moban = ShangjiaMoban.objects.create(
- yonghu_id=yonghu_id,
- leixing_id=leixing_id,
- moban_jieshao=jieshao,
- jiage=jiage_decimal,
- fabu_shuliang=0,
- #zhuangtai=1
- )
-
- # 8. 返回成功响应(与前端字段对应)
- return Response({
- 'code': 200,
- 'msg': '模板创建成功',
- 'data': {
- 'mobanId': moban.id, # 模板ID
- 'shangpinTypeId': leixing_id, # 商品类型ID
- 'jieshao': moban.moban_jieshao, # 模板介绍
- 'jiage': str(moban.jiage), # 价格(转为字符串)
- 'fabushuliang': moban.fabu_shuliang, # 发布数量
- 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S') # 创建时间
- }
- })
-
- except Exception as e:
- # 记录错误日志
- import logging
- logger = logging.getLogger(__name__)
- logger.error(f"创建商家模板失败: {str(e)}", exc_info=True)
-
- return Response({
- 'code': 500,
- 'msg': '服务器内部错误',
- 'data': {}
- }, status=500)
-
-
-# views/shangjia_moban.py
-from django.core.paginator import Paginator
-from django.db.models import Q
-
-
-class ShangjiaMobanListView(APIView):
- """
- 商家订单模板列表接口
- POST /peizhi/sjddmblb
- 权限: JWT认证
- 功能: 支持普通分页和搜索筛选
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- user = request.user
- yonghu_id = user.yonghuid
-
- # 获取参数
- leixing_id = request.data.get('shangpinTypeId')
- if not leixing_id:
- return Response({'code': 400, 'msg': '商品类型ID不能为空', 'data': {}}, status=400)
-
- get_type = int(request.data.get('getType', 2))
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 50))
-
- # 搜索参数
- keyword = request.data.get('keyword', '')
- min_price = request.data.get('minPrice', '')
- # 修复 labelId 可能为整数,统一转为整数,避免 .strip() 错误
- label_id = request.data.get('labelId', None)
- if label_id is not None:
- try:
- label_id = int(label_id)
- except (ValueError, TypeError):
- label_id = None
-
- # 构建基础查询
- query = Q(yonghu_id=yonghu_id, leixing_id=leixing_id)
-
- # 搜索模式处理
- if get_type == 1:
- if keyword and keyword.strip():
- keyword = keyword.strip()
- # 先尝试按模板ID精确搜索,同时按介绍模糊搜索
- try:
- moban_id_int = int(keyword)
- query &= Q(id=moban_id_int) | Q(moban_jieshao__icontains=keyword)
- except ValueError:
- query &= Q(moban_jieshao__icontains=keyword)
- if min_price:
- try:
- min_p = Decimal(str(min_price))
- query &= Q(jiage__gte=min_p)
- except:
- pass
- if label_id:
- query &= Q(chenghao_id=label_id)
-
- # 搜索模式不分页,返回所有匹配结果
- moban_list = ShangjiaMoban.objects.filter(query).order_by('-id').values(
- 'id', 'leixing_id', 'moban_jieshao', 'jiage', 'fabu_shuliang',
- 'yongjin_qiangdan', 'yongjin_jiage', 'chenghao_id', 'create_time'
- )
-
- # 批量获取称号名称
- chenghao_ids = [m['chenghao_id'] for m in moban_list if m.get('chenghao_id')]
- chenghao_map = {}
- if chenghao_ids:
- chs = Chenghao.objects.filter(id__in=chenghao_ids).values('id', 'mingcheng')
- chenghao_map = {ch['id']: ch['mingcheng'] for ch in chs}
-
- formatted = []
- for item in moban_list:
- formatted.append({
- 'mobanId': item['id'],
- 'shangpinTypeId': item['leixing_id'],
- 'jieshao': item['moban_jieshao'],
- 'jiage': str(item['jiage']),
- 'fabushuliang': item['fabu_shuliang'],
- 'commissionEnabled': item['yongjin_qiangdan'],
- 'commissionValue': str(item['yongjin_jiage']) if item['yongjin_jiage'] is not None else None,
- 'labelId': item['chenghao_id'],
- 'labelName': chenghao_map.get(item['chenghao_id'], ''),
- 'createTime': item['create_time'].strftime('%Y-%m-%d %H:%M:%S') if item['create_time'] else ''
- })
-
- return Response({
- 'code': 200,
- 'msg': '搜索成功',
- 'data': {
- 'list': formatted,
- 'total': len(formatted),
- 'hasMore': False
- }
- })
-
- # 普通分页模式
- queryset = ShangjiaMoban.objects.filter(query).order_by('-id')
- paginator = Paginator(queryset, page_size)
- if page < 1 or page > paginator.num_pages:
- return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400)
-
- current_page = paginator.page(page)
- moban_list = current_page.object_list.values(
- 'id', 'leixing_id', 'moban_jieshao', 'jiage', 'fabu_shuliang',
- 'yongjin_qiangdan', 'yongjin_jiage', 'chenghao_id', 'create_time'
- )
-
- # 批量获取称号名称
- chenghao_ids = [m['chenghao_id'] for m in moban_list if m.get('chenghao_id')]
- chenghao_map = {}
- if chenghao_ids:
- chs = Chenghao.objects.filter(id__in=chenghao_ids).values('id', 'mingcheng')
- chenghao_map = {ch['id']: ch['mingcheng'] for ch in chs}
-
- formatted = []
- for item in moban_list:
- formatted.append({
- 'mobanId': item['id'],
- 'shangpinTypeId': item['leixing_id'],
- 'jieshao': item['moban_jieshao'],
- 'jiage': str(item['jiage']),
- 'fabushuliang': item['fabu_shuliang'],
- 'commissionEnabled': item['yongjin_qiangdan'],
- 'commissionValue': str(item['yongjin_jiage']) if item['yongjin_jiage'] is not None else None,
- 'labelId': item['chenghao_id'],
- 'labelName': chenghao_map.get(item['chenghao_id'], ''),
- 'createTime': item['create_time'].strftime('%Y-%m-%d %H:%M:%S') if item['create_time'] else ''
- })
-
- has_more = current_page.has_next()
- # 如果当前页数据不足page_size,强制hasMore=False
- if len(formatted) < page_size:
- has_more = False
-
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': {
- 'list': formatted,
- 'total': paginator.count,
- 'hasMore': has_more,
- 'currentPage': page,
- 'pageSize': page_size,
- 'totalPages': paginator.num_pages
- }
- })
-
- except Exception as e:
- logger.error(f"获取商家模板列表失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
-
-
-# views/shangjia_moban.py
-class ShangjiaGengxinMobanView(APIView):
- """
- 商家更新订单模板接口
- POST /peizhi/sjddmbgx
- 权限: JWT认证用户
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 获取用户ID
- user = request.user
- yonghu_id = user.yonghuid
-
- # 2. 获取请求参数
- moban_id = request.data.get('mobanId')
- leixing_id = request.data.get('shangpinTypeId')
- new_jieshao = request.data.get('jieshao', '').strip()
- new_jiage = request.data.get('jiage', '')
-
- # 3. 验证必填字段
- if not all([moban_id, leixing_id]):
- return Response({
- 'code': 400,
- 'msg': '模板ID和商品类型ID不能为空',
- 'data': {}
- }, status=400)
-
- # 4. 检查是否有修改内容
- if not new_jieshao and not new_jiage:
- return Response({
- 'code': 400,
- 'msg': '没有修改内容',
- 'data': {}
- }, status=400)
-
- # 5. 验证修改内容
- update_fields = {}
-
- # 验证介绍
- if new_jieshao:
- if len(new_jieshao) > 100:
- return Response({
- 'code': 400,
- 'msg': '订单介绍不能超过100字',
- 'data': {}
- }, status=400)
- update_fields['moban_jieshao'] = new_jieshao
-
- # 验证价格
- if new_jiage:
- try:
- jiage_decimal = Decimal(new_jiage)
- if jiage_decimal <= 0 or jiage_decimal > 10000:
- return Response({
- 'code': 400,
- 'msg': '价格必须在0.1-10000元之间',
- 'data': {}
- }, status=400)
- update_fields['jiage'] = jiage_decimal
- except (ValueError, TypeError):
- return Response({
- 'code': 400,
- 'msg': '价格格式错误',
- 'data': {}
- }, status=400)
-
- # 6. 查询模板是否存在且属于当前用户
- try:
- moban = ShangjiaMoban.objects.get(
- id=moban_id,
- yonghu_id=yonghu_id,
- leixing_id=leixing_id,
- #zhuangtai=1 # 只修改正常状态的模板
- )
- except ShangjiaMoban.DoesNotExist:
- return Response({
- 'code': 404,
- 'msg': '模板不存在或无权修改',
- 'data': {}
- }, status=404)
-
- # 7. 检查介绍是否重复(如果修改了介绍)
- if 'moban_jieshao' in update_fields:
- existing_moban = ShangjiaMoban.objects.filter(
- yonghu_id=yonghu_id,
- leixing_id=leixing_id,
- moban_jieshao=update_fields['moban_jieshao'],
- #zhuangtai=1
- ).exclude(id=moban_id).exists() # 排除自身
-
- if existing_moban:
- return Response({
- 'code': 400,
- 'msg': '已存在相同介绍的模板',
- 'data': {}
- }, status=400)
-
- # 8. 更新模板(使用update_fields优化,只更新修改的字段)
- with transaction.atomic():
- for field, value in update_fields.items():
- setattr(moban, field, value)
- moban.save(update_fields=list(update_fields.keys()))
-
- # 9. 获取更新后的数据
- updated_moban = ShangjiaMoban.objects.filter(id=moban_id).values(
- 'id', 'leixing_id', 'moban_jieshao', 'jiage', 'fabu_shuliang', 'update_time'
- ).first()
-
- # 10. 返回成功响应
- return Response({
- 'code': 200,
- 'msg': '模板修改成功',
- 'data': {
- 'mobanId': updated_moban['id'],
- 'shangpinTypeId': updated_moban['leixing_id'],
- 'jieshao': updated_moban['moban_jieshao'],
- 'jiage': str(updated_moban['jiage']),
- 'fabushuliang': updated_moban['fabu_shuliang'],
- 'updateTime': updated_moban['update_time'].strftime('%Y-%m-%d %H:%M:%S') if updated_moban[
- 'update_time'] else ''
- }
- })
-
- except Exception as e:
- # 记录错误日志
- import logging
- logger = logging.getLogger(__name__)
- logger.error(f"修改商家模板失败: {str(e)}", exc_info=True)
-
- return Response({
- 'code': 500,
- 'msg': '服务器内部错误',
- 'data': {}
- }, status=500)
-
-
-# views/shangjia_moban.py
-# views/shangjia_moban.py
-class ShangjiaShanchuMobanView(APIView):
- """
- 删除模板
- POST /peizhi/sjddmbsc
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- user = request.user
- yonghu_id = user.yonghuid
-
- moban_id = request.data.get('mobanId')
- leixing_id = request.data.get('shangpinTypeId')
-
- if not all([moban_id, leixing_id]):
- return Response({'code': 400, 'msg': '参数缺失', 'data': {}}, status=400)
-
- try:
- moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id, leixing_id=leixing_id)
- except ShangjiaMoban.DoesNotExist:
- return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404)
-
- # 强制硬删除
- moban.delete()
-
- return Response({'code': 200, 'msg': '删除成功', 'data': {'mobanId': moban_id, 'shangpinTypeId': leixing_id}})
- except Exception as e:
- import logging
- logger = logging.getLogger(__name__)
- logger.error(f"删除模板失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
-
-
-import logging
-
-from peizhi.models import ShangjiaMoban, ShangjiaLianjie
-from shangpin.models import ShangpinLeixing
-from dingdan.models import Dingdan, DingdanShangjia
-# 假设利率表模型路径(根据你的项目结构调整)
-from dingdan.models import Lilubiao
-
-logger = logging.getLogger(__name__)
-
-# views/peizhi/sjljhq.py
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from rest_framework import status
-from django.db import transaction
-from django.db.models import F
-from django.conf import settings
-from django.utils import timezone
-import time
-import random
-import os
-import logging
-import hashlib
-import secrets
-from decimal import Decimal
-
-logger = logging.getLogger(__name__)
-
-
-class ShangjiaGenerateLinkView(APIView):
- """商家生成发单链接接口"""
- permission_classes = [IsAuthenticated] # JWT Token认证
-
- def post(self, request):
- # 1. 记录请求日志
- logger.info(f"商家生成链接请求 - 用户ID: {request.user.yonghuid}, 数据: {request.data}")
-
- # 2. 获取前端参数
- data = request.data
- shangpin_type_id = data.get('shangpinTypeId') # 商品类型ID
- moban_id = data.get('mobanId') # 模板ID
-
- # 3. 基本参数验证
- if not all([shangpin_type_id, moban_id]):
- logger.warning(f"参数缺失 - shangpin_type_id: {shangpin_type_id}, moban_id: {moban_id}")
- return Response({
- 'code': 400,
- 'msg': '缺少必填参数:订单类型ID和模板ID',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- shangpin_type_id = int(shangpin_type_id)
- moban_id = int(moban_id)
- except ValueError:
- return Response({
- 'code': 400,
- 'msg': '参数格式错误,必须是数字',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 获取商家扩展信息(使用反向查询,最高效的方式)
- try:
- shangjia = request.user.shop_profile
- except UserShangjia.DoesNotExist:
- logger.error(f"商家扩展信息不存在 - 用户ID: {request.user.yonghuid}")
- return Response({
- 'code': 403,
- 'msg': '您不是商家或商家信息未完善',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 5. 验证商家状态
- if shangjia.zhuangtai != 1:
- logger.warning(f"商家状态异常 - 用户ID: {request.user.yonghuid}, 状态: {shangjia.zhuangtai}")
- return Response({
- 'code': 403,
- 'msg': '商家账号状态异常,请联系管理员',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 6. 查询商品类型信息
- try:
- shangpin_leixing = ShangpinLeixing.objects.get(id=shangpin_type_id)
- except ShangpinLeixing.DoesNotExist:
- logger.error(f"商品类型不存在 - 类型ID: {shangpin_type_id}")
- return Response({
- 'code': 400,
- 'msg': '商品类型不存在',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 提取商品类型信息
- yaoqiuleixing = shangpin_leixing.yaoqiuleixing
- huiyuan_id = shangpin_leixing.huiyuan_id if shangpin_leixing.huiyuan_id else ''
- # yongjin 字段暂时不需要用到
-
- # 7. 查询商家订单模板(使用组合查询,最高效)
- try:
- moban = ShangjiaMoban.objects.get(
- yonghu_id=request.user.yonghuid,
- leixing_id=shangpin_type_id,
- id=moban_id
- )
- except ShangjiaMoban.DoesNotExist:
- logger.error(
- f"商家模板不存在 - 用户ID: {request.user.yonghuid}, 类型ID: {shangpin_type_id}, 模板ID: {moban_id}")
- return Response({
- 'code': 400,
- 'msg': '订单模板不存在',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取模板价格和介绍
- jiage = moban.jiage
- dingdan_jieshao = moban.moban_jieshao
-
- # 8. 验证商家余额
- if jiage > shangjia.yue:
- logger.warning(f"商家余额不足 - 用户ID: {request.user.yonghuid}, 余额: {shangjia.yue}, 价格: {jiage}")
- return Response({
- 'code': 400,
- 'msg': f'余额不足,当前余额{shangjia.yue}元',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 9. 查询利率(复用商家派单接口逻辑)
- try:
- lilu_obj = Lilubiao.objects.get(fadanpingtai='3')
- lilu = float(lilu_obj.lilu)
- if lilu <= 0:
- lilu = 1 # 默认值
- logger.info(f"获取到利率 - 平台: 3, 利率: {lilu}")
- except Lilubiao.DoesNotExist:
- logger.warning("利率表不存在,使用默认利率1")
- lilu = 1
-
- # 10. 计算打手分成
- # 方法1:最安全的方式,兼容所有情况
- dashou_fencheng = Decimal(str(round(float(jiage) * lilu, 2)))
- logger.info(f"计算打手分成 - 价格: {jiage}, 利率: {lilu}, 分成: {dashou_fencheng}")
-
- # 11. 生成订单ID(复用商家派单接口的逻辑)
- def generate_dingdan_id():
- """生成唯一订单ID"""
- timestamp = int(time.time() * 1000)
- process_id = os.getpid() % 1000
- random_num = random.randint(1000, 9999)
- return f"SJ{timestamp}{process_id:03d}{random_num}"
-
- # 🔴🆕【新增】生成安全Token的函数
- def generate_secure_token(dingdan_id, shangjia_id):
- """
- 生成安全的Token
- 算法:时间戳 + 订单ID + 商家ID + 随机数 → SHA256 → 截取前24位
- """
- timestamp = int(time.time() * 1_000_000) # 纳秒级精度
- random_salt = secrets.token_hex(8) # 16位随机盐值
-
- # 组合原始字符串
- raw_string = f"{timestamp}:{dingdan_id}:{shangjia_id}:{random_salt}"
-
- # 使用SHA256哈希
- hash_obj = hashlib.sha256(raw_string.encode('utf-8'))
- token = hash_obj.hexdigest()[:24] # 取前24位作为Token
-
- # 确保唯一性(理论上极小概率重复,但还是要检查)
- try:
- if ShangjiaLianjie.objects.filter(lianjie_token=token).exists():
- # 如果重复,重新生成(极低概率)
- return generate_secure_token(dingdan_id, shangjia_id)
- except:
- pass
-
- return token
-
- # 🔴🆕【修改】生成链接函数 - 使用Token而不是订单ID
- def generate_link_url(token):
- """生成安全的H5页面链接 - 使用Token而不是订单ID"""
- base_url = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com')
- # 确保base_url以/结尾
- if not base_url.endswith('/'):
- base_url += '/'
- # 🔴🆕【关键修改】生成使用Token的链接
- return f"{base_url}order/{token}"
-
- # 13. 使用事务确保数据一致性(这是最关键的步骤!)
- try:
- with transaction.atomic():
- # 生成订单ID
- dingdan_id = generate_dingdan_id()
-
- # 🔴🆕【新增】生成安全Token
- secure_token = generate_secure_token(dingdan_id, request.user.yonghuid)
-
- # 创建订单主表(状态为13:灰度状态,等待老板填写)
- dingdan = Dingdan.objects.create(
- dingdan_id=dingdan_id,
- zhuangtai=13, # 🔴 核心:状态13表示灰度状态,等待老板填写
- fadan_pingtai=2, # 商家发单
- jine=jiage,
- dashou_fencheng=dashou_fencheng,
- # 以下字段留空,等待老板填写
- jiedan_dashou_id='',
- dashou_liuyan='',
- zhiding_id='',
- shangpin_id=0, # 暂时设为0
- leixing_id=shangpin_type_id,
- yaoqiuleixing=yaoqiuleixing if yaoqiuleixing else 1,
- huiyuan_id=huiyuan_id,
- yongjin=None, # 暂时不需要
- jieshao=dingdan_jieshao[:2000], # 限制长度
- beizhu='', # 备注为空,等待老板填写
- nicheng='', # 🔴 游戏昵称为空,等待老板填写
- tupian=request.user.avatar if request.user.avatar else '',
- user1_id=f"Sj{request.user.yonghuid}", # 商家标识
- user2_id='', # 打手标识为空
- )
- logger.info(f"创建订单成功 - 订单ID: {dingdan_id}")
-
- # 创建商家订单扩展表
- DingdanShangjia.objects.create(
- dingdan=dingdan,
- shangjia_id=request.user.yonghuid,
- sjnicheng=shangjia.nicheng if shangjia.nicheng else f"商家{request.user.yonghuid}",
- shangjia_pingjia='',
- cfliyou='',
- sqzhuangtai=0,
- bhliyou=''
- )
- logger.info(f"创建商家订单扩展表成功 - 订单ID: {dingdan_id}")
-
- # 🔴 核心:更新商家统计信息和余额(使用F表达式,避免并发问题)
- UserShangjia.objects.filter(id=shangjia.id).update(
- fabu=F('fabu') + 1,
- yue=F('yue') - jiage, # 🔴 扣除余额
- jinridingdan=F('jinridingdan') + 1,
- jinriliushui=F('jinriliushui') + jiage,
- jinyuedingdan=F('jinyuedingdan') + 1,
- jinyueliushui=F('jinyueliushui') + jiage,
- )
- logger.info(f"更新商家统计信息成功 - 用户ID: {request.user.yonghuid}, 扣除金额: {jiage}")
-
- # 🔴🆕【修改】生成链接 - 使用Token而不是订单ID
- link_url = generate_link_url(secure_token)
- logger.info(f"生成链接 - Token: {secure_token}, 链接: {link_url}")
-
- # 🔴🆕【修改】保存链接到商家链接表 - 添加Token字段
- lianjie_obj = ShangjiaLianjie.objects.create(
- yonghu_id=request.user.yonghuid,
- leixing_id=shangpin_type_id,
- moban_id=moban_id,
- lianjie=link_url,
- lianjie_token=secure_token, # 🔴🆕【新增】保存Token
- dingdan_id=dingdan_id,
- is_used=False, # 未使用
- youxiao_shijian=timezone.now() + timezone.timedelta(days=1) # 24小时有效
- )
- logger.info(f"保存链接到数据库成功 - 链接ID: {lianjie_obj.id}, Token: {secure_token}")
-
- # 更新模板发布数量
- ShangjiaMoban.objects.filter(id=moban_id).update(
- fabu_shuliang=F('fabu_shuliang') + 1,
- )
- logger.info(f"更新模板发布数量成功 - 模板ID: {moban_id}")
-
- # 重新获取商家最新数据(因为余额已更新)
- shangjia.refresh_from_db()
-
- # 记录成功日志
- logger.info(
- f"商家生成链接成功 - 用户ID: {request.user.yonghuid}, 订单ID: {dingdan_id}, Token: {secure_token}, 链接: {link_url}")
-
- # 14. 返回成功响应(字段必须与前端对应)
- return Response({
- 'code': 200,
- 'msg': '链接生成成功',
- 'data': {
- 'linkUrl': link_url, # 🔴 前端需要这个字段
- 'newBalance': str(shangjia.yue), # 🔴 返回新的余额给前端更新
- 'dingdanId': dingdan_id,
- 'dingdanJieshao': dingdan_jieshao,
- 'jiage': str(jiage),
- 'youxiaoShijian': lianjie_obj.youxiao_shijian.strftime(
- '%Y-%m-%d %H:%M:%S') if lianjie_obj.youxiao_shijian else '',
- # 以下字段可根据前端需要添加
- 'jinriliushui': str(shangjia.jinriliushui),
- 'jinyueliushui': str(shangjia.jinyueliushui),
- 'jinridingdan': shangjia.jinridingdan,
- 'jinyuedingdan': shangjia.jinyuedingdan
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f"生成链接失败 - 用户ID: {request.user.yonghuid}, 错误: {str(e)}", exc_info=True)
- return Response({
- 'code': 500,
- 'msg': '系统错误,生成链接失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)'''
-
-
-
-
-
-
-from django.core.paginator import Paginator
-from django.db.models import Q, Prefetch
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from peizhi.models import ShangjiaMoban
-from dengji.models import Chenghao
-
-class ShangjiaMobanListView(APIView):
- """
- 商家订单模板列表接口
- POST /peizhi/sjddmblb
- 权限: JWT认证
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- user = request.user
- yonghu_id = user.yonghuid
-
- leixing_id = request.data.get('shangpinTypeId')
- if not leixing_id:
- return Response({'code': 400, 'msg': '商品类型ID不能为空', 'data': {}}, status=400)
-
- get_type = int(request.data.get('getType', 2))
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 50))
-
- keyword = request.data.get('keyword', '')
- min_price = request.data.get('minPrice', '')
- label_id = request.data.get('labelId', None)
- if label_id is not None:
- try:
- label_id = int(label_id)
- except (ValueError, TypeError):
- label_id = None
-
- query = Q(yonghu_id=yonghu_id, leixing_id=leixing_id)
-
- if get_type == 1: # 搜索模式
- if keyword:
- keyword = str(keyword).strip()
- try:
- mid = int(keyword)
- query &= Q(id=mid) | Q(moban_jieshao__icontains=keyword)
- except ValueError:
- query &= Q(moban_jieshao__icontains=keyword)
- if min_price:
- try:
- query &= Q(jiage__gte=Decimal(str(min_price)))
- except:
- pass
- if label_id:
- query &= Q(chenghao_id=label_id)
-
- mobans = ShangjiaMoban.objects.filter(query).order_by('-id')
- formatted = []
- for moban in mobans:
- texiao_json = ''
- label_name = ''
- if moban.chenghao_id:
- try:
- ch = Chenghao.objects.get(id=moban.chenghao_id)
- label_name = ch.mingcheng
- texiao_json = ch.texiao_miaoshu or ''
- except Chenghao.DoesNotExist:
- pass
- formatted.append({
- 'mobanId': moban.id,
- 'shangpinTypeId': moban.leixing_id,
- 'jieshao': moban.moban_jieshao,
- 'jiage': str(moban.jiage),
- 'fabushuliang': moban.fabu_shuliang,
- 'commissionEnabled': moban.yongjin_qiangdan,
- 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None,
- 'labelId': moban.chenghao_id,
- 'labelName': label_name,
- 'texiaoJson': texiao_json,
- 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S') if moban.create_time else ''
- })
- return Response({'code': 200, 'msg': '搜索成功', 'data': {'list': formatted, 'total': len(formatted), 'hasMore': False}})
-
- # 分页模式
- queryset = ShangjiaMoban.objects.filter(query).order_by('-id')
- paginator = Paginator(queryset, page_size)
- if page < 1 or page > paginator.num_pages:
- return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400)
-
- current_page = paginator.page(page)
- formatted = []
- for moban in current_page.object_list:
- texiao_json = ''
- label_name = ''
- if moban.chenghao_id:
- try:
- ch = Chenghao.objects.get(id=moban.chenghao_id)
- label_name = ch.mingcheng
- texiao_json = ch.texiao_miaoshu or ''
- except Chenghao.DoesNotExist:
- pass
- formatted.append({
- 'mobanId': moban.id,
- 'shangpinTypeId': moban.leixing_id,
- 'jieshao': moban.moban_jieshao,
- 'jiage': str(moban.jiage),
- 'fabushuliang': moban.fabu_shuliang,
- 'commissionEnabled': moban.yongjin_qiangdan,
- 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None,
- 'labelId': moban.chenghao_id,
- 'labelName': label_name,
- 'texiaoJson': texiao_json,
- 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S') if moban.create_time else ''
- })
-
- has_more = current_page.has_next()
- if len(formatted) < page_size:
- has_more = False
-
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': {
- 'list': formatted,
- 'total': paginator.count,
- 'hasMore': has_more,
- 'currentPage': page,
- 'pageSize': page_size,
- 'totalPages': paginator.num_pages
- }
- })
-
- except Exception as e:
- logger.error(f"获取模板列表失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
-
-from dengji.models import Chenghao
-
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from django.db.models import Prefetch
-class ShangjiaTianjiaMobanView(APIView):
- """
- 商家添加订单模板接口
- POST /peizhi/sjtjddmb
- 权限: JWT认证
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 获取当前商家用户ID
- user = request.user
- yonghu_id = user.yonghuid
-
- # 2. 接收并清洗参数,避免类型错误
- leixing_id = request.data.get('shangpinTypeId')
- jieshao = request.data.get('jieshao', '').strip() if request.data.get('jieshao') else ''
- jiage = request.data.get('jiage', '')
- # 佣金相关
- commission_enabled = request.data.get('commissionEnabled', False)
- commission_value = request.data.get('commissionValue', None)
- # 标签ID(称号ID)
- label_id = request.data.get('labelId', None)
-
- # 3. 必填验证
- if not leixing_id or not jieshao or not jiage:
- logger.warning(f"参数缺失 - leixing_id:{leixing_id}, jieshao:{jieshao}, jiage:{jiage}")
- return Response({
- 'code': 400,
- 'msg': '缺少必填字段(订单类型、介绍、价格)',
- 'data': {}
- }, status=400)
-
- # 4. 价格格式验证
- try:
- jiage_decimal = Decimal(str(jiage))
- if jiage_decimal <= 0:
- return Response({'code': 400, 'msg': '价格必须大于0', 'data': {}}, status=400)
- if jiage_decimal > 10000:
- return Response({'code': 400, 'msg': '价格不能超过10000元', 'data': {}}, status=400)
- except (InvalidOperation, ValueError, TypeError):
- return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400)
-
- # 5. 介绍长度验证
- if len(jieshao) > 100:
- return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400)
-
- # 6. 佣金验证(若启用)
- yongjin_qiangdan = False
- yongjin_jiage = None
- if commission_enabled:
- yongjin_qiangdan = True
- if not commission_value:
- return Response({'code': 400, 'msg': '开启佣金时,佣金金额不能为空', 'data': {}}, status=400)
- try:
- comm_decimal = Decimal(str(commission_value))
- if comm_decimal <= 0:
- return Response({'code': 400, 'msg': '佣金必须大于0', 'data': {}}, status=400)
- yongjin_jiage = comm_decimal
- except (InvalidOperation, ValueError, TypeError):
- return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400)
-
- # 7. 标签ID清洗(前端可能传数字或字符串)
- cleaned_label_id = None
- if label_id is not None and str(label_id).strip():
- try:
- cleaned_label_id = int(label_id)
- except (ValueError, TypeError):
- return Response({'code': 400, 'msg': '标签ID格式错误', 'data': {}}, status=400)
-
- # 8. 验证商品类型是否存在且可用
- try:
- shangpin_obj = ShangpinLeixing.objects.get(
- id=leixing_id,
- shenhezhuangtai=1 # 审核通过
- )
- except ShangpinLeixing.DoesNotExist:
- return Response({'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=404)
-
- # 9. 检查同一用户、同一类型下是否已存在相同介绍的模板
- if ShangjiaMoban.objects.filter(
- yonghu_id=yonghu_id,
- leixing_id=leixing_id,
- moban_jieshao=jieshao
- ).exists():
- return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400)
-
- # 10. 执行数据库创建(事务)
- with transaction.atomic():
- moban = ShangjiaMoban.objects.create(
- yonghu_id=yonghu_id,
- leixing_id=leixing_id,
- moban_jieshao=jieshao,
- jiage=jiage_decimal,
- fabu_shuliang=0,
- yongjin_qiangdan=yongjin_qiangdan,
- yongjin_jiage=yongjin_jiage,
- chenghao_id=cleaned_label_id
- )
-
- # 11. 返回成功数据(字段与前端对齐)
- return Response({
- 'code': 200,
- 'msg': '模板创建成功',
- 'data': {
- 'mobanId': moban.id,
- 'shangpinTypeId': leixing_id,
- 'jieshao': moban.moban_jieshao,
- 'jiage': str(moban.jiage),
- 'fabushuliang': moban.fabu_shuliang,
- 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S'),
- 'commissionEnabled': moban.yongjin_qiangdan,
- 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None,
- 'labelId': moban.chenghao_id
- }
- })
-
- except Exception as e:
- # 打印完整错误堆栈到日志
- logger.error(f"添加模板失败: {str(e)}", exc_info=True)
- return Response({
- 'code': 500,
- 'msg': '服务器内部错误,添加模板失败,请联系管理员',
- 'data': {}
- }, status=500)
-
-
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from peizhi.models import ShangjiaMoban
-
-class ShangjiaShanchuMobanView(APIView):
- """
- 删除模板
- POST /peizhi/sjddmbsc
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- user = request.user
- yonghu_id = user.yonghuid
-
- moban_id = request.data.get('mobanId')
- leixing_id = request.data.get('shangpinTypeId')
-
- if not all([moban_id, leixing_id]):
- return Response({'code': 400, 'msg': '参数缺失', 'data': {}}, status=400)
-
- try:
- moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id, leixing_id=leixing_id)
- except ShangjiaMoban.DoesNotExist:
- return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404)
-
- # 强制硬删除
- moban.delete()
-
- return Response({'code': 200, 'msg': '删除成功', 'data': {'mobanId': moban_id, 'shangpinTypeId': leixing_id}})
- except Exception as e:
- import logging
- logger = logging.getLogger(__name__)
- logger.error(f"删除模板失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
-
-
-
-
-from decimal import Decimal, InvalidOperation
-from django.db import transaction
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from peizhi.models import ShangjiaMoban
-
-class ShangjiaGengxinMobanView(APIView):
- """
- 商家更新订单模板接口
- POST /peizhi/sjddmbgx
- 权限: JWT认证
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- user = request.user
- yonghu_id = user.yonghuid
-
- moban_id = request.data.get('mobanId')
- leixing_id = request.data.get('shangpinTypeId')
- new_jieshao = request.data.get('jieshao', '').strip()
- new_jiage = request.data.get('jiage', '').strip()
- # 新增字段
- commission_enabled = request.data.get('commissionEnabled')
- commission_value = request.data.get('commissionValue')
- label_id = request.data.get('labelId')
-
- if not all([moban_id, leixing_id]):
- return Response({'code': 400, 'msg': '模板ID和商品类型ID不能为空', 'data': {}}, status=400)
-
- # 查询模板
- try:
- moban = ShangjiaMoban.objects.get(
- id=moban_id,
- yonghu_id=yonghu_id,
- leixing_id=leixing_id
- )
- except ShangjiaMoban.DoesNotExist:
- return Response({'code': 404, 'msg': '模板不存在或无权修改', 'data': {}}, status=404)
-
- update_fields = []
-
- # 修改介绍
- if new_jieshao:
- if len(new_jieshao) > 100:
- return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400)
- if new_jieshao != moban.moban_jieshao:
- moban.moban_jieshao = new_jieshao
- update_fields.append('moban_jieshao')
-
- # 修改价格
- if new_jiage:
- try:
- jiage_decimal = Decimal(new_jiage)
- if jiage_decimal <= 0 or jiage_decimal > 10000:
- return Response({'code': 400, 'msg': '价格必须在0.1-10000元之间', 'data': {}}, status=400)
- if jiage_decimal != moban.jiage:
- moban.jiage = jiage_decimal
- update_fields.append('jiage')
- except (InvalidOperation, ValueError, TypeError):
- return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400)
-
- # 修改标签
- if label_id is not None: # 允许传空字符串或不传,传0表示清空
- new_label = int(label_id) if label_id else None
- if new_label != moban.chenghao_id:
- moban.chenghao_id = new_label
- update_fields.append('chenghao_id')
-
- # 修改佣金设置
- if commission_enabled is not None:
- enabled = bool(commission_enabled)
- if enabled:
- if not commission_value:
- return Response({'code': 400, 'msg': '佣金价格不能为空', 'data': {}}, status=400)
- try:
- comm_decimal = Decimal(commission_value)
- if comm_decimal < 0.1 or comm_decimal > 50:
- return Response({'code': 400, 'msg': '佣金需在0.1-50元之间', 'data': {}}, status=400)
- if moban.yongjin_qiangdan != True or moban.yongjin_jiage != comm_decimal:
- moban.yongjin_qiangdan = True
- moban.yongjin_jiage = comm_decimal
- update_fields.extend(['yongjin_qiangdan', 'yongjin_jiage'])
- except (InvalidOperation, ValueError, TypeError):
- return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400)
- else:
- if moban.yongjin_qiangdan != False or moban.yongjin_jiage is not None:
- moban.yongjin_qiangdan = False
- moban.yongjin_jiage = None
- update_fields.extend(['yongjin_qiangdan', 'yongjin_jiage'])
-
- if not update_fields:
- return Response({'code': 400, 'msg': '没有修改内容', 'data': {}}, status=400)
-
- # 检查介绍唯一性
- if 'moban_jieshao' in update_fields:
- if ShangjiaMoban.objects.filter(
- yonghu_id=yonghu_id,
- leixing_id=leixing_id,
- moban_jieshao=moban.moban_jieshao
- ).exclude(id=moban_id).exists():
- return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400)
-
- with transaction.atomic():
- moban.save(update_fields=update_fields)
-
- # 返回最新数据
- return Response({
- 'code': 200,
- 'msg': '模板修改成功',
- 'data': {
- 'mobanId': moban.id,
- 'shangpinTypeId': moban.leixing_id,
- 'jieshao': moban.moban_jieshao,
- 'jiage': str(moban.jiage),
- 'fabushuliang': moban.fabu_shuliang,
- 'commissionEnabled': moban.yongjin_qiangdan,
- 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage else None,
- 'labelId': moban.chenghao_id,
- 'updateTime': moban.update_time.strftime('%Y-%m-%d %H:%M:%S')
- }
- })
-
- except Exception as e:
- import logging
- logger = logging.getLogger(__name__)
- logger.error(f"修改商家模板失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
-
-
-
-from dengji.models import DingdanBiaoqian
-
-
-
-import random # 必须导入
-import secrets
-
-import os
-import time
-import random # 必须导入
-import hashlib
-import secrets
-import logging
-from decimal import Decimal
-from django.conf import settings
-from django.db import transaction
-from django.db.models import F
-from django.utils import timezone
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework import status
-from yonghu.models import UserShangjia
-
-from shangpin.models import ShangpinLeixing
-
-from dengji.models import Chenghao
-
-logger = logging.getLogger(__name__)
-
-class ShangjiaGenerateLinkView(APIView):
- """商家生成发单链接接口(支持模板佣金/标签)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- user = request.user
- # 获取参数
- shangpin_type_id = request.data.get('shangpinTypeId')
- moban_id = request.data.get('mobanId')
- if not all([shangpin_type_id, moban_id]):
- return Response({'code': 400, 'msg': '缺少参数', 'data': {}}, status=400)
-
- try:
- shangpin_type_id = int(shangpin_type_id)
- moban_id = int(moban_id)
- except ValueError:
- return Response({'code': 400, 'msg': '参数格式错误', 'data': {}}, status=400)
-
- # 商家信息
- try:
- shangjia = user.shop_profile
- except UserShangjia.DoesNotExist:
- return Response({'code': 403, 'msg': '商家信息未完善', 'data': {}}, status=403)
- if shangjia.zhuangtai != 1:
- return Response({'code': 403, 'msg': '商家状态异常', 'data': {}}, status=403)
-
- # 商品类型
- try:
- shangpin_leixing = ShangpinLeixing.objects.get(id=shangpin_type_id, shenhezhuangtai=1)
- except ShangpinLeixing.DoesNotExist:
- return Response({'code': 400, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=400)
-
- # 查询模板
- try:
- moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=user.yonghuid, leixing_id=shangpin_type_id)
- except ShangjiaMoban.DoesNotExist:
- return Response({'code': 400, 'msg': '模板不存在', 'data': {}}, status=400)
-
- jiage = moban.jiage
- if jiage > shangjia.yue:
- return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元', 'data': {}}, status=400)
-
- # 利率
- try:
- lilu = float(Lilubiao.objects.get(fadanpingtai='3').lilu) or 1.0
- except Lilubiao.DoesNotExist:
- lilu = 1.0
- dashou_fencheng = Decimal(str(round(float(jiage) * lilu, 2)))
-
- # 抢单要求:根据模板是否启用佣金
- chenghao_id = moban.chenghao_id
- if moban.yongjin_qiangdan and moban.yongjin_jiage is not None:
- final_yaoqiuleixing = 2 # 佣金模式
- final_yongjin = moban.yongjin_jiage
- final_huiyuan_id = ''
- else:
- final_yaoqiuleixing = shangpin_leixing.yaoqiuleixing or 1
- final_yongjin = None
- final_huiyuan_id = shangpin_leixing.huiyuan_id or ''
-
- # 获取标签名称
- label_name = ''
- if chenghao_id:
- try:
- label_name = Chenghao.objects.get(id=chenghao_id).mingcheng
- except Chenghao.DoesNotExist:
- pass
-
- # 生成ID和Token
- def generate_dingdan_id():
- timestamp = int(time.time() * 1000)
- process_id = os.getpid() % 1000
- random_num = random.randint(1000, 9999) # 注意需要 import random
- return f"SJ{timestamp}{process_id:03d}{random_num}"
-
- def generate_secure_token(dd_id, uid):
- timestamp = int(time.time() * 1_000_000)
- salt = secrets.token_hex(8)
- raw = f"{timestamp}:{dd_id}:{uid}:{salt}"
- token = hashlib.sha256(raw.encode()).hexdigest()[:24]
- # 确保唯一性(极小概率重复再试一次)
- if ShangjiaLianjie.objects.filter(lianjie_token=token).exists():
- return generate_secure_token(dd_id, uid)
- return token
-
- def generate_link_url(token):
- base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/')
- return f"{base}/order/{token}"
-
- with transaction.atomic():
- dingdan_id = generate_dingdan_id()
- secure_token = generate_secure_token(dingdan_id, user.yonghuid)
-
- # 创建订单主表
- dingdan = Dingdan.objects.create(
- dingdan_id=dingdan_id,
- zhuangtai=13,
- fadan_pingtai=2,
- jine=jiage,
- dashou_fencheng=dashou_fencheng,
- leixing_id=shangpin_type_id,
- yaoqiuleixing=final_yaoqiuleixing,
- huiyuan_id=final_huiyuan_id,
- yongjin=final_yongjin,
- jieshao=moban.moban_jieshao[:2000],
- user1_id=f"Sj{user.yonghuid}",
- tupian=user.avatar or '',
- )
-
- # 商家扩展表
- DingdanShangjia.objects.create(
- dingdan=dingdan,
- shangjia_id=user.yonghuid,
- sjnicheng=shangjia.nicheng or f"商家{user.yonghuid}",
- )
-
- # 关联标签
- if chenghao_id:
- try:
- ch = Chenghao.objects.get(id=chenghao_id)
- DingdanBiaoqian.objects.create(dingdan=dingdan, chenghao=ch)
- except Chenghao.DoesNotExist:
- logger.warning(f"称号{chenghao_id}不存在,跳过关联")
-
- # 更新商家余额和统计
- UserShangjia.objects.filter(id=shangjia.id).update(
- fabu=F('fabu') + 1,
- yue=F('yue') - jiage,
- jinridingdan=F('jinridingdan') + 1,
- jinriliushui=F('jinriliushui') + jiage,
- jinyuedingdan=F('jinyuedingdan') + 1,
- jinyueliushui=F('jinyueliushui') + jiage,
- )
-
- # 生成链接并保存
- link_url = generate_link_url(secure_token)
- lianjie_obj = ShangjiaLianjie.objects.create(
- yonghu_id=user.yonghuid,
- leixing_id=shangpin_type_id,
- moban_id=moban_id,
- lianjie=link_url,
- lianjie_token=secure_token,
- dingdan_id=dingdan_id,
- is_used=False,
- youxiao_shijian=timezone.now() + timezone.timedelta(days=1)
- )
-
- # 更新模板发布数量
- ShangjiaMoban.objects.filter(id=moban_id).update(fabu_shuliang=F('fabu_shuliang') + 1)
- shangjia.refresh_from_db()
-
- logger.info(f"链接生成成功 - 订单ID: {dingdan_id}, 标签: {label_name}")
-
- return Response({
- 'code': 200,
- 'msg': '链接生成成功',
- 'data': {
- 'linkUrl': link_url,
- 'newBalance': str(shangjia.yue),
- 'dingdanId': dingdan_id,
- 'dingdanJieshao': moban.moban_jieshao,
- 'jiage': str(jiage),
- 'labelId': chenghao_id,
- 'labelName': label_name,
- 'youxiaoShijian': lianjie_obj.youxiao_shijian.strftime('%Y-%m-%d %H:%M:%S'),
- }
- })
-
- except Exception as e:
- logger.error(f"生成链接事务失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '系统错误,生成链接失败', 'data': {}}, status=500)
-
-
-from django.core.paginator import Paginator
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from peizhi.models import ShangjiaLianjie
-
-class ShangjiaLianjieListView(APIView):
- """获取已生成的链接列表(支持筛选是否使用)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- user = request.user
- yonghu_id = user.yonghuid
-
- moban_id = request.data.get('mobanId')
- used = int(request.data.get('used', 0)) # 0未使用,1已使用
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 5))
- page_size = min(page_size, 50)
-
- if not moban_id:
- return Response({'code': 400, 'msg': '模板ID不能为空', 'data': {}}, status=400)
-
- # 校验模板归属
- try:
- moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id)
- except ShangjiaMoban.DoesNotExist:
- return Response({'code': 404, 'msg': '模板不存在或无权查看', 'data': {}}, status=404)
-
- links = ShangjiaLianjie.objects.filter(
- yonghu_id=yonghu_id,
- moban_id=moban_id,
- is_used=(used == 1)
- ).order_by('-create_time')
-
- paginator = Paginator(links, page_size)
- if page < 1 or page > paginator.num_pages:
- return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400)
-
- current_page = paginator.page(page)
- data_list = []
- for link in current_page.object_list:
- data_list.append({
- 'id': link.id,
- 'lianjie': link.lianjie,
- 'lianjie_token': link.lianjie_token,
- 'is_used': link.is_used,
- 'dingdan_id': link.dingdan_id,
- 'youxiao_shijian': link.youxiao_shijian.strftime('%Y-%m-%d %H:%M:%S') if link.youxiao_shijian else '',
- 'create_time': link.create_time.strftime('%Y-%m-%d %H:%M:%S')
- })
-
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': {
- 'list': data_list,
- 'total': paginator.count,
- 'hasMore': current_page.has_next(),
- 'currentPage': page,
- 'pageSize': page_size
- }
- })
-
- except Exception as e:
- import logging
- logger = logging.getLogger(__name__)
- logger.error(f"获取链接列表失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
-
-
-
-
-# utils/ip_security.py
-import logging
-import re
-from django.utils import timezone
-from django.core.cache import cache
-from rest_framework.throttling import SimpleRateThrottle
-
-logger = logging.getLogger(__name__)
-
-
-class IPUtils:
- """IP工具类"""
-
- @staticmethod
- def get_client_ip(request):
- """
- 获取客户端真实IP,处理代理情况
- 优先级: X-Real-IP > X-Forwarded-For > remote_addr
- """
- ip = None
-
- try:
- # 1. 尝试从 X-Real-IP 获取
- x_real_ip = request.META.get('HTTP_X_REAL_IP')
- if x_real_ip:
- ip = x_real_ip.strip()
- logger.info(f"从 X-Real-IP 获取IP: {ip}")
- return ip
-
- # 2. 尝试从 X-Forwarded-For 获取(处理代理链)
- x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
- if x_forwarded_for:
- # 可能有多个代理IP,第一个是真实IP
- ips = x_forwarded_for.split(',')
- for possible_ip in ips:
- possible_ip = possible_ip.strip()
- if possible_ip and possible_ip.lower() != 'unknown':
- ip = possible_ip
- logger.info(f"从 X-Forwarded-For 获取IP: {ip}")
- return ip
-
- # 3. 使用 remote_addr
- ip = request.META.get('REMOTE_ADDR', '')
- logger.info(f"从 REMOTE_ADDR 获取IP: {ip}")
-
- except Exception as e:
- logger.error(f"获取IP失败: {str(e)}", exc_info=True)
- ip = '0.0.0.0'
-
- return ip
-
- @staticmethod
- def validate_ip(ip):
- """验证IP格式是否合法"""
- if not ip:
- return False
-
- # IPv4正则
- ipv4_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
-
- # IPv6正则(简化版)
- ipv6_pattern = r'^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$'
-
- return bool(re.match(ipv4_pattern, ip) or re.match(ipv6_pattern, ip))
-
- @staticmethod
- def is_private_ip(ip):
- """判断是否为内网IP"""
- if not ip:
- return False
-
- # 内网IP段
- private_ranges = [
- ('10.0.0.0', '10.255.255.255'),
- ('172.16.0.0', '172.31.255.255'),
- ('192.168.0.0', '192.168.255.255'),
- ('127.0.0.0', '127.255.255.255'),
- ]
-
- try:
- # 将IP转换为整数比较
- def ip_to_int(ip):
- parts = ip.split('.')
- return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3])
-
- ip_num = ip_to_int(ip)
- for start, end in private_ranges:
- if ip_to_int(start) <= ip_num <= ip_to_int(end):
- return True
- except:
- pass
-
- return False
-
-
-class OrderLinkThrottle(SimpleRateThrottle):
- """订单链接访问频率限制"""
-
- scope = 'order_link'
- rate = '10/minute' # 每分钟最多10次
-
- def get_cache_key(self, request, view):
- # 基于IP和接口路径进行限制
- ip = IPUtils.get_client_ip(request)
- if not ip or IPUtils.is_private_ip(ip):
- # 内网IP或不合法IP,使用默认
- ip = 'default'
-
- return f'throttle_{view.__class__.__name__}_{ip}'
-
-
-# views/peizhi/khhqddlj.py
-import logging
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from django.utils import timezone
-from django.db.models import Q
-from .models import ShangjiaLianjie
-from dingdan.models import Dingdan, DingdanShangjia
-from yonghu.models import UserMain, UserShangjia
-
-
-logger = logging.getLogger(__name__)
-# views.py - KehuGetDingdanLianjieView 修改部分
-
-
-from peizhi.models import ClubConfig
-
-
-
-
-'''class KehuGetDingdanLianjieView(APIView):
- """
- 客户获取订单链接信息接口
- 路径:GET /peizhi/khhqddlj?token=xxx
- 返回字段包括:订单信息、打手标识、商家标识、俱乐部配置(客服链接、存储桶、GoEasy AppKey等)
- """
-
- throttle_classes = [OrderLinkThrottle]
- permission_classes = [] # 无需认证,任何人都可访问
-
- def get(self, request):
- """
- 处理GET请求,获取订单链接信息
- """
- # 1. 记录请求开始
- logger.info("=== 客户获取订单链接信息接口开始 ===")
-
- # 2. 获取客户端真实IP
- client_ip = IPUtils.get_client_ip(request)
- logger.info(f"客户端IP: {client_ip}")
-
- # 3. 验证IP合法性
- if not IPUtils.validate_ip(client_ip):
- logger.warning(f"IP格式不合法: {client_ip}")
- return Response({
- 'code': 400,
- 'msg': '网络连接异常',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 获取Token参数
- token = request.query_params.get('token', '')
- if not token:
- logger.warning(f"Token参数缺失 - IP: {client_ip}")
- return Response({
- 'code': 400,
- 'msg': '缺少链接参数',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- logger.info(f"接收到Token: {token[:8]}...")
-
- # 5. 查询链接记录
- try:
- lianjie_obj = ShangjiaLianjie.objects.select_related().get(lianjie_token=token)
- logger.info(f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}")
- except ShangjiaLianjie.DoesNotExist:
- logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}")
- return Response({
- 'code': 404,
- 'msg': '链接不存在或已失效',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"查询链接异常: {str(e)}", exc_info=True)
- return Response({
- 'code': 500,
- 'msg': '系统错误,请稍后重试',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 6. 更新访问者IP(无论是否已使用,都记录最新访问IP)
- try:
- lianjie_obj.shiyongzhe_ip = client_ip
- lianjie_obj.save(update_fields=['shiyongzhe_ip'])
- logger.info(f"更新访问者IP成功: {client_ip}")
- except Exception as e:
- logger.warning(f"更新IP失败: {str(e)}")
- # 继续处理,不影响主要逻辑
-
- # 7. 检查链接有效期
- """now = timezone.now()
- if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now:
- logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}, 当前: {now}")
- return Response({
- 'code': 400,
- 'msg': '链接已过期',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)"""
-
- # 8. 准备返回数据
- response_data = {
- 'is_used': lianjie_obj.is_used,
- 'dingdan_id': lianjie_obj.dingdan_id or '',
- 'youxiao_shijian': lianjie_obj.youxiao_shijian.strftime(
- '%Y-%m-%d %H:%M:%S') if lianjie_obj.youxiao_shijian else '',
- }
-
- # 9. 获取订单信息
- if lianjie_obj.dingdan_id:
- try:
- # 查询订单主表
- dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id)
-
- # 获取商家昵称
- shangjia_nicheng = ''
- try:
- user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id)
- shangjia_obj = user_main.shop_profile
- shangjia_nicheng = shangjia_obj.nicheng
- except (UserMain.DoesNotExist, UserShangjia.DoesNotExist):
- logger.warning(f"商家信息不存在 - 用户ID: {lianjie_obj.yonghu_id}")
-
- # ========== 新增:确定俱乐部配置 ==========
- # 默认头像(相对路径)
- default_avatar = 'a_long/morentouxiang.jpg'
- # 初始化配置字段默认值
- kefu_link = ''
- oss_url = ''
- goeasy_appkey = ''
-
- # 判断是否为跨平台订单
- is_cross = dingdan_obj.is_cross if hasattr(dingdan_obj, 'is_cross') else 0
-
- # 默认使用我方配置
- use_self_config = True
- target_club_id = None
-
- if is_cross == 1:
- # 跨平台订单
- # 优先使用 partner_club_id
- if dingdan_obj.partner_club_id:
- target_club_id = dingdan_obj.partner_club_id
- use_self_config = False
- logger.info(f"跨平台订单,使用对方俱乐部ID: {target_club_id}")
- # 其次,如果有对方订单ID但没有俱乐部ID,使用默认俱乐部ID 000001
- elif dingdan_obj.partner_order_id:
- target_club_id = '000001'
- use_self_config = False
- logger.info(f"跨平台订单有对方订单ID但无俱乐部ID,使用默认俱乐部ID: {target_club_id}")
- else:
- # 无任何对方信息,降级为我方配置
- use_self_config = True
- logger.warning(f"订单标记跨平台但无对方俱乐部ID和对方订单ID,使用我方配置")
- else:
- # 非跨平台订单
- use_self_config = True
-
- # 获取俱乐部配置
- club_config = None
- if use_self_config:
- # 查询我方俱乐部配置 (is_self=1)
- try:
- club_config = ClubConfig.objects.filter(is_self=1).first()
- if club_config:
- logger.info(f"使用我方俱乐部配置,club_id={club_config.club_id}")
- else:
- logger.warning("未找到我方俱乐部配置(is_self=1)")
- except Exception as e:
- logger.error(f"查询我方俱乐部配置异常: {str(e)}", exc_info=True)
- else:
- # 使用确定的俱乐部ID查询对方配置
- if target_club_id:
- try:
- club_config = ClubConfig.objects.filter(club_id=target_club_id).first()
- if club_config:
- logger.info(f"使用对方俱乐部配置,club_id={target_club_id}")
- else:
- logger.warning(f"未找到对方俱乐部配置,club_id={target_club_id}")
- except Exception as e:
- logger.error(f"查询对方俱乐部配置异常: {str(e)}", exc_info=True)
-
- if club_config:
- kefu_link = club_config.customer_service_api_url or ''
- oss_url = club_config.storage_bucket_domain or ''
- goeasy_appkey = club_config.chat_api_key or ''
-
- # ========== 构建订单信息 ==========
- dingdan_info = {
- 'jieshao': dingdan_obj.jieshao or '',
- 'jiage': str(dingdan_obj.jine) if dingdan_obj.jine is not None else '0.00', # 金额字段名是 jine
- 'shangjia_mingcheng': shangjia_nicheng,
- 'zhuangtai': dingdan_obj.zhuangtai,
- 'is_cross': is_cross,
- # 新增配置字段
- 'kefu_link': kefu_link,
- 'oss_url': oss_url,
- 'default_avatar': default_avatar,
- 'goeasy_appkey': goeasy_appkey,
- }
-
- # 如果已使用,返回已填写的信息
- if lianjie_obj.is_used:
- dingdan_info.update({
- 'youxi_nicheng': dingdan_obj.nicheng or '',
- 'zhiding_dashou': dingdan_obj.zhiding_id or '',
- 'beizhu': dingdan_obj.beizhu or '',
- })
-
- # 获取聊天相关标识
- # 9.1 获取接单打手ID
- jiedan_dashou_id = dingdan_obj.jiedan_dashou_id
- dashou_biaoshi = f'Ds{jiedan_dashou_id}' if jiedan_dashou_id else ''
-
- # 9.2 获取商家标识
- shangjia_biaoshi = ''
- try:
- shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').filter(
- dingdan__dingdan_id=lianjie_obj.dingdan_id
- ).first()
-
- if shangjia_kuozhan and shangjia_kuozhan.shangjia_id:
- shangjia_biaoshi = f'Sj{shangjia_kuozhan.shangjia_id}'
- logger.info(f"生成商家标识: {shangjia_biaoshi}")
- else:
- logger.info(f"订单 {lianjie_obj.dingdan_id} 没有商家订单扩展信息")
- except Exception as e:
- logger.error(f"查询商家订单扩展异常: {str(e)}", exc_info=True)
-
- # 添加聊天标识
- dingdan_info.update({
- 'dashou_biaoshi': dashou_biaoshi,
- 'shangjia_biaoshi': shangjia_biaoshi,
- 'you_dashou': bool(dashou_biaoshi),
- })
-
- response_data.update(dingdan_info)
-
- except Dingdan.DoesNotExist:
- logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}")
- response_data.update({
- 'jieshao': '',
- 'jiage': '0.00',
- 'shangjia_mingcheng': '',
- 'zhuangtai': None,
- 'dashou_biaoshi': '',
- 'shangjia_biaoshi': '',
- 'you_dashou': False,
- 'is_cross': 0,
- 'kefu_link': '',
- 'oss_url': '',
- 'default_avatar': 'a_long/morentouxiang.jpg',
- 'goeasy_appkey': '',
- })
- except Exception as e:
- logger.error(f"获取订单信息异常: {str(e)}", exc_info=True)
- response_data.update({
- 'jieshao': '',
- 'jiage': '0.00',
- 'shangjia_mingcheng': '',
- 'zhuangtai': None,
- 'dashou_biaoshi': '',
- 'shangjia_biaoshi': '',
- 'you_dashou': False,
- 'is_cross': 0,
- 'kefu_link': '',
- 'oss_url': '',
- 'default_avatar': 'a_long/morentouxiang.jpg',
- 'goeasy_appkey': '',
- })
- else:
- # 如果订单ID为空
- logger.warning(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}")
- response_data.update({
- 'jieshao': '',
- 'jiage': '0.00',
- 'shangjia_mingcheng': '',
- 'zhuangtai': None,
- 'dashou_biaoshi': '',
- 'shangjia_biaoshi': '',
- 'you_dashou': False,
- 'is_cross': 0,
- 'kefu_link': '',
- 'oss_url': '',
- 'default_avatar': 'a_long/morentouxiang.jpg',
- 'goeasy_appkey': '',
- })
-
- # 10. 判断订单状态是否允许修改
- if not lianjie_obj.is_used and response_data.get('zhuangtai') == 13:
- response_data['allow_modify'] = True
- else:
- response_data['allow_modify'] = False
-
- # 11. 记录成功日志并返回
- logger.info(
- f"返回订单信息成功 - Token: {token[:8]}..., IP: {client_ip}, "
- f"是否允许修改: {response_data['allow_modify']}, "
- f"是否有打手: {response_data.get('you_dashou', False)}"
- )
-
- return Response({
- 'code': 200,
- 'msg': '成功',
- 'data': response_data
- }, status=status.HTTP_200_OK)'''
-
-
-
-
-
-
-
-class KehuGetDingdanLianjieView(APIView):
- """
- 客户获取订单链接信息接口
- 路径:GET /peizhi/khhqddlj?token=xxx
- 返回字段包括:订单信息、打手标识、商家标识、俱乐部配置(客服链接、存储桶、GoEasy AppKey等)
- """
-
- throttle_classes = [OrderLinkThrottle]
- permission_classes = [] # 无需认证,任何人都可访问
-
- def get(self, request):
- """
- 处理GET请求,获取订单链接信息
- """
- # 1. 记录请求开始
- logger.info("=== 客户获取订单链接信息接口开始 ===")
-
- # 2. 获取客户端真实IP
- client_ip = IPUtils.get_client_ip(request)
- logger.info(f"客户端IP: {client_ip}")
-
- # 3. 验证IP合法性
- if not IPUtils.validate_ip(client_ip):
- logger.warning(f"IP格式不合法: {client_ip}")
- return Response({
- 'code': 400,
- 'msg': '网络连接异常',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 获取Token参数
- token = request.query_params.get('token', '')
- if not token:
- logger.warning(f"Token参数缺失 - IP: {client_ip}")
- return Response({
- 'code': 400,
- 'msg': '缺少链接参数',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- logger.info(f"接收到Token: {token[:8]}...")
-
- # 5. 查询链接记录
- try:
- lianjie_obj = ShangjiaLianjie.objects.select_related().get(lianjie_token=token)
- logger.info(f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}")
- except ShangjiaLianjie.DoesNotExist:
- logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}")
- return Response({
- 'code': 404,
- 'msg': '链接不存在或已失效',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"查询链接异常: {str(e)}", exc_info=True)
- return Response({
- 'code': 500,
- 'msg': '系统错误,请稍后重试',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 6. 更新访问者IP(无论是否已使用,都记录最新访问IP)
- try:
- lianjie_obj.shiyongzhe_ip = client_ip
- lianjie_obj.save(update_fields=['shiyongzhe_ip'])
- logger.info(f"更新访问者IP成功: {client_ip}")
- except Exception as e:
- logger.warning(f"更新IP失败: {str(e)}")
- # 继续处理,不影响主要逻辑
-
- # 7. 检查链接有效期
- '''now = timezone.now()
- if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now:
- logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}, 当前: {now}")
- return Response({
- 'code': 400,
- 'msg': '链接已过期',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)'''
-
- # 8. 准备返回数据
- response_data = {
- 'is_used': lianjie_obj.is_used,
- 'dingdan_id': lianjie_obj.dingdan_id or '',
- 'youxiao_shijian': lianjie_obj.youxiao_shijian.strftime(
- '%Y-%m-%d %H:%M:%S') if lianjie_obj.youxiao_shijian else '',
- }
-
- # 9. 获取订单信息
- if lianjie_obj.dingdan_id:
- try:
- # 查询订单主表
- dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id)
-
- # 获取商家昵称
- shangjia_nicheng = ''
- try:
- user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id)
- shangjia_obj = user_main.shop_profile
- shangjia_nicheng = shangjia_obj.nicheng
- except (UserMain.DoesNotExist, UserShangjia.DoesNotExist):
- logger.warning(f"商家信息不存在 - 用户ID: {lianjie_obj.yonghu_id}")
-
- # ========== 新增:确定俱乐部配置 ==========
- # 默认头像(相对路径)
- default_avatar = 'a_long/morentouxiang.jpg'
- # 初始化配置字段默认值
- kefu_link = ''
- oss_url = ''
- goeasy_appkey = ''
-
- # 判断是否为跨平台订单(保留,但不影响配置选择)
- is_cross = dingdan_obj.is_cross if hasattr(dingdan_obj, 'is_cross') else 0
-
- # 始终使用我方俱乐部配置
- try:
- club_config = ClubConfig.objects.filter(is_self=1).first()
- if club_config:
- logger.info(f"使用我方俱乐部配置,club_id={club_config.club_id}")
- kefu_link = club_config.customer_service_api_url or ''
- oss_url = club_config.storage_bucket_domain or ''
- goeasy_appkey = club_config.chat_api_key or ''
- else:
- logger.warning("未找到我方俱乐部配置(is_self=1)")
- except Exception as e:
- logger.error(f"查询我方俱乐部配置异常: {str(e)}", exc_info=True)
-
- # ========== 构建订单信息 ==========
- dingdan_info = {
- 'jieshao': dingdan_obj.jieshao or '',
- 'jiage': str(dingdan_obj.jine) if dingdan_obj.jine is not None else '0.00', # 金额字段名是 jine
- 'shangjia_mingcheng': shangjia_nicheng,
- 'zhuangtai': dingdan_obj.zhuangtai,
- 'is_cross': is_cross,
- # 新增配置字段
- 'kefu_link': kefu_link,
- 'oss_url': oss_url,
- 'default_avatar': default_avatar,
- 'goeasy_appkey': goeasy_appkey,
- # 🆕 前端用于连接GoEasy的自身用户ID(固定B前缀+商家ID)
- 'self_user_id': f"B{lianjie_obj.yonghu_id}" if lianjie_obj.yonghu_id else '',
- }
-
- # 如果已使用,返回已填写的信息
- if lianjie_obj.is_used:
- dingdan_info.update({
- 'youxi_nicheng': dingdan_obj.nicheng or '',
- 'zhiding_dashou': dingdan_obj.zhiding_id or '',
- 'beizhu': dingdan_obj.beizhu or '',
- })
-
- # 获取聊天相关标识
- # 9.1 获取接单打手ID
- jiedan_dashou_id = dingdan_obj.jiedan_dashou_id
- dashou_biaoshi = f'Ds{jiedan_dashou_id}' if jiedan_dashou_id else ''
-
- # 9.2 获取商家标识
- shangjia_biaoshi = ''
- try:
- shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').filter(
- dingdan__dingdan_id=lianjie_obj.dingdan_id
- ).first()
-
- if shangjia_kuozhan and shangjia_kuozhan.shangjia_id:
- shangjia_biaoshi = f'Sj{shangjia_kuozhan.shangjia_id}'
- logger.info(f"生成商家标识: {shangjia_biaoshi}")
- else:
- logger.info(f"订单 {lianjie_obj.dingdan_id} 没有商家订单扩展信息")
- except Exception as e:
- logger.error(f"查询商家订单扩展异常: {str(e)}", exc_info=True)
-
- # 添加聊天标识
- dingdan_info.update({
- 'dashou_biaoshi': dashou_biaoshi,
- 'shangjia_biaoshi': shangjia_biaoshi,
- 'you_dashou': bool(dashou_biaoshi),
- })
-
- response_data.update(dingdan_info)
-
- except Dingdan.DoesNotExist:
- logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}")
- response_data.update({
- 'jieshao': '',
- 'jiage': '0.00',
- 'shangjia_mingcheng': '',
- 'zhuangtai': None,
- 'dashou_biaoshi': '',
- 'shangjia_biaoshi': '',
- 'you_dashou': False,
- 'is_cross': 0,
- 'kefu_link': '',
- 'oss_url': '',
- 'default_avatar': 'a_long/morentouxiang.jpg',
- 'goeasy_appkey': '',
- 'self_user_id': '',
- })
- except Exception as e:
- logger.error(f"获取订单信息异常: {str(e)}", exc_info=True)
- response_data.update({
- 'jieshao': '',
- 'jiage': '0.00',
- 'shangjia_mingcheng': '',
- 'zhuangtai': None,
- 'dashou_biaoshi': '',
- 'shangjia_biaoshi': '',
- 'you_dashou': False,
- 'is_cross': 0,
- 'kefu_link': '',
- 'oss_url': '',
- 'default_avatar': 'a_long/morentouxiang.jpg',
- 'goeasy_appkey': '',
- 'self_user_id': '',
- })
- else:
- # 如果订单ID为空
- logger.warning(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}")
- response_data.update({
- 'jieshao': '',
- 'jiage': '0.00',
- 'shangjia_mingcheng': '',
- 'zhuangtai': None,
- 'dashou_biaoshi': '',
- 'shangjia_biaoshi': '',
- 'you_dashou': False,
- 'is_cross': 0,
- 'kefu_link': '',
- 'oss_url': '',
- 'default_avatar': 'a_long/morentouxiang.jpg',
- 'goeasy_appkey': '',
- 'self_user_id': '',
- })
-
- # 10. 判断订单状态是否允许修改
- if not lianjie_obj.is_used and response_data.get('zhuangtai') == 13:
- response_data['allow_modify'] = True
- else:
- response_data['allow_modify'] = False
-
- # 11. 记录成功日志并返回
- logger.info(
- f"返回订单信息成功 - Token: {token[:8]}..., IP: {client_ip}, "
- f"是否允许修改: {response_data['allow_modify']}, "
- f"是否有打手: {response_data.get('you_dashou', False)}"
- )
-
- return Response({
- 'code': 200,
- 'msg': '成功',
- 'data': response_data
- }, status=status.HTTP_200_OK)
-
-
-
-
-
-import threading
-from utils.weixin_broadcast import WeixinBroadcastSender
-from dingdan.utils import sync_order_to_partners
-logger = logging.getLogger(__name__)
-
-
-
-class KehuTianxieDingdanView(APIView):
- """
- 客户填写订单信息接口
- 路径:POST /peizhi/kehutx
- """
-
- # 添加频率限制(比查询接口更严格)
- throttle_classes = [OrderLinkThrottle]
- permission_classes = [] # 无需认证
-
- def post(self, request):
- """
- 处理POST请求,填写订单信息
- """
- # 1. 记录请求开始
- logger.info("=== 客户填写订单信息接口开始 ===")
-
- # 2. 获取客户端真实IP
- client_ip = IPUtils.get_client_ip(request)
- logger.info(f"客户端IP: {client_ip}")
-
- # 3. 验证IP合法性
- if not IPUtils.validate_ip(client_ip):
- logger.warning(f"IP格式不合法: {client_ip}")
- return Response({
- 'code': 400,
- 'msg': '网络连接异常',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 获取请求参数
- data = request.data
- token = data.get('token', '')
- youxi_nicheng = data.get('youxi_nicheng', '').strip()
- zhiding_dashou = data.get('zhiding_dashou', '').strip() # 可选
- beizhu = data.get('beizhu', '').strip() # 可选
-
-
- # 4.0 临时关闭指定功能(如需开启,请删除或注释掉下面两行)
- #if zhiding_dashou:
- #return Response({'code': 8, 'msg': '指定功能尚未开启,请不要填写指定打手ID', 'data': None})
- #if beizhu:
- #return Response({'code': 8, 'msg': '备注功能尚未开启,请不要填写备注', 'data': None})
-
-
- logger.info(
- f"请求参数 - Token: {token[:8]}..., 昵称: {youxi_nicheng[:10]}..., 指定打手: {zhiding_dashou}, 备注长度: {len(beizhu)}")
-
- # 5. 验证必填参数
- if not token:
- logger.warning(f"Token参数缺失 - IP: {client_ip}")
- return Response({
- 'code': 400,
- 'msg': '缺少链接参数',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not youxi_nicheng:
- logger.warning(f"游戏昵称为空 - IP: {client_ip}")
- return Response({
- 'code': 400,
- 'msg': '游戏昵称不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 6. 验证游戏昵称格式
- if len(youxi_nicheng) > 50:
- logger.warning(f"游戏昵称过长 - IP: {client_ip}, 长度: {len(youxi_nicheng)}")
- return Response({
- 'code': 400,
- 'msg': '游戏昵称不能超过50个字符',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 过滤危险字符
- danger_chars = ['<', '>', "'", '"', '&', ';', '|']
- for char in danger_chars:
- if char in youxi_nicheng:
- logger.warning(f"游戏昵称包含危险字符 - IP: {client_ip}, 字符: {char}")
- return Response({
- 'code': 400,
- 'msg': '昵称包含非法字符',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 7. 验证指定打手ID格式(如果提供)
- if zhiding_dashou and len(zhiding_dashou) > 32:
- logger.warning(f"指定打手ID过长 - IP: {client_ip}, 长度: {len(zhiding_dashou)}")
- return Response({
- 'code': 400,
- 'msg': '指定打手ID不能超过32个字符',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 8. 验证备注长度
- if len(beizhu) > 500:
- logger.warning(f"备注过长 - IP: {client_ip}, 长度: {len(beizhu)}")
- return Response({
- 'code': 400,
- 'msg': '备注不能超过500个字符',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 9. 查询链接记录
- try:
- lianjie_obj = ShangjiaLianjie.objects.get(lianjie_token=token)
- logger.info(
- f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}, 是否已使用: {lianjie_obj.is_used}")
- except ShangjiaLianjie.DoesNotExist:
- logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}")
- return Response({
- 'code': 404,
- 'msg': '链接不存在或已失效',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 10. 检查链接有效期
- now = timezone.now()
- if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now:
- logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}")
- return Response({
- 'code': 400,
- 'msg': '链接已过期',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 11. 检查链接是否已使用
- if lianjie_obj.is_used:
- logger.warning(f"链接已使用 - 链接ID: {lianjie_obj.id}, IP: {client_ip}")
- return Response({
- 'code': 400,
- 'msg': '链接已被使用,无法重复提交',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 12. 验证指定打手(如果需要)
- zhuangtai_xin = 1 # 默认普通订单状态
- if zhiding_dashou:
- # 验证打手是否存在且有效
- try:
- # 先查询用户主表
- dashou_user = UserMain.objects.get(yonghuid=zhiding_dashou)
- logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}")
-
- # 通过反向关系获取打手扩展信息
- try:
- dashou_profile = dashou_user.dashou_profile
-
- # 检查打手状态(zhuangtai)
- if dashou_profile.zhuangtai != 1:
- logger.warning(f"打手状态异常 - 打手ID: {zhiding_dashou}, 状态: {dashou_profile.zhuangtai}")
- return Response({
- 'code': 400,
- 'msg': '指定打手当前无法接单',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 检查打手账号状态(zhanghaozhuangtai)
- if dashou_profile.zhanghaozhuangtai != 1:
- logger.warning(
- f"打手账号状态异常 - 打手ID: {zhiding_dashou}, 账号状态: {dashou_profile.zhanghaozhuangtai}")
- return Response({
- 'code': 400,
- 'msg': '指定打手账号异常',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证通过,设置为指定订单状态
- zhuangtai_xin = 7 # 指定打手订单状态
- logger.info(f"指定打手验证通过 - 打手ID: {zhiding_dashou}, 昵称: {dashou_profile.nicheng}")
-
- except UserDashou.DoesNotExist:
- logger.warning(f"打手扩展信息不存在 - 用户ID: {zhiding_dashou}")
- return Response({
- 'code': 400,
- 'msg': '指定打手信息不完整',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- except UserMain.DoesNotExist:
- logger.warning(f"指定打手用户不存在 - 用户ID: {zhiding_dashou}")
- return Response({
- 'code': 400,
- 'msg': '指定打手不存在',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 13. 使用事务确保数据一致性
- try:
- with transaction.atomic():
- # 更新链接记录
- lianjie_obj.shiyongzhe_ip = client_ip
- lianjie_obj.is_used = True
- lianjie_obj.save(update_fields=['shiyongzhe_ip', 'is_used'])
- logger.info(f"更新链接记录成功 - 链接ID: {lianjie_obj.id}, 设置为已使用")
-
- # 查询订单
- if not lianjie_obj.dingdan_id:
- logger.error(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}")
- raise ValueError("订单ID为空")
-
- dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id)
-
- # 检查订单状态是否为13(灰色状态)
- if dingdan_obj.zhuangtai != 13:
- logger.warning(
- f"订单状态不允许修改 - 订单ID: {dingdan_obj.dingdan_id}, 当前状态: {dingdan_obj.zhuangtai}")
- # 回滚事务
- transaction.set_rollback(True)
- return Response({
- 'code': 400,
- 'msg': '订单状态异常,无法修改',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 更新订单信息
- dingdan_obj.nicheng = youxi_nicheng # 游戏昵称
- dingdan_obj.beizhu = beizhu if beizhu else '' # 备注
- dingdan_obj.zhuangtai = zhuangtai_xin # 新状态:1或7
-
- # 如果是指定打手,设置zhiding_id
- if zhiding_dashou and zhuangtai_xin == 7:
- dingdan_obj.zhiding_id = zhiding_dashou
-
- dingdan_obj.save()
- logger.info(
- f"更新订单成功 - 订单ID: {dingdan_obj.dingdan_id}, 新状态: {zhuangtai_xin}, 指定打手: {zhiding_dashou or '无'}")
-
- # 🔴【添加】在订单更新成功后,异步发送微信服务号通知(原有)
- #🟢【7. 新增】支付成功后的广播通知(异步,不阻塞主流程)
- from dingdan.tongzhi_tasks import dingdan_guangbo
-
-
- # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用)
- order_info = {
- 'dingdan_id': dingdan_obj.dingdan_id,
- 'game_type': self._get_game_type_name(dingdan_obj.leixing_id),
- 'amount': str(dingdan_obj.jine),
- 'order_desc': (dingdan_obj.jieshao or '')[:50],
- }
- dingdan_guangbo.delay(order_info) # 瞬间返回,不阻塞请求
-
- # 获取商家信息用于返回
- try:
- user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id)
- shangjia_obj = user_main.shop_profile
- shangjia_mingcheng = shangjia_obj.nicheng
- except:
- shangjia_mingcheng = f"商家{lianjie_obj.yonghu_id}"
- logger.warning(f"获取商家昵称失败,使用默认 - 用户ID: {lianjie_obj.yonghu_id}")
-
- # ========== 🆕 新增:判断是否需要跨平台同步 ==========
- # 获取商家ID(从链接记录)
- shangjia_user_id = lianjie_obj.yonghu_id
- # 跨平台条件:商家ID在白名单、订单为普通状态(zhuangtai=1)、要求类型为1、会员ID等于配置值
- sync_order_to_partners(dingdan_obj.dingdan_id)
- '''
- if (shangjia_user_id in settings.CROSS_PLATFORM_WHITELIST and
- dingdan_obj.zhuangtai == 1 and # 非指定单
- dingdan_obj.yaoqiuleixing == 1 and
- dingdan_obj.huiyuan_id in ['VC7CTU', 'VUXP8D', 'VEX5NJ']
- ):
-
- # 异步发送通知(使用独立模块)
- import threading # 若文件顶部已导入可省略,此处保留确保独立运行
- from dingdan.cross_platform_tasks import send_partner_sync_order
- thread = threading.Thread(
- target=send_partner_sync_order,
- args=(
- dingdan_obj.dingdan_id,
- str(dingdan_obj.jine),
- str(dingdan_obj.dashou_fencheng),
- dingdan_obj.jieshao,
- dingdan_obj.beizhu,
- dingdan_obj.create_time.isoformat(),
- dingdan_obj.nicheng
-
- )
- )
- thread.daemon = True
- thread.start()
- logger.info(f"跨平台客户填写订单异步任务已启动: {dingdan_obj.dingdan_id}")'''
- # ========== 🆕 新增结束 ==========
-
- # 14. 返回成功响应
- response_data = {
- #'dingdan_id': dingdan_obj.dingdan_id,
- #'create_time': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S'),
- #'shangpin_mingcheng': dingdan_obj.jieshao[:50] if dingdan_obj.jieshao else '', # 截取前50字符
- #'jiage': str(dingdan_obj.jine) if dingdan_obj.jine else '0.00',
- 'shangjia_mingcheng': shangjia_mingcheng,
- 'youxi_nicheng': youxi_nicheng,
- 'zhuangtai': zhuangtai_xin,
- 'zhuangtai_text': '普通订单' if zhuangtai_xin == 1 else '指定打手订单'
- }
-
- logger.info(f"订单提交成功 - 订单ID: {dingdan_obj.dingdan_id}, IP: {client_ip}")
-
- # ✅ 新方法(行为驱动,更安全)
- from houtai.utils import update_shangjia_daily
- update_shangjia_daily(
- yonghuid=lianjie_obj.yonghu_id,
- amount=Decimal(str(dingdan_obj.jine)),
- action=1 # 1 = 派发
- )
-
- return Response({
- 'code': 200,
- 'msg': '提交成功',
- 'data': response_data
- }, status=status.HTTP_200_OK)
-
- except Dingdan.DoesNotExist:
- logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}")
- return Response({
- 'code': 500,
- 'msg': '订单数据异常',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- except Exception as e:
- logger.error(f"处理订单异常: {str(e)}", exc_info=True)
- return Response({
- 'code': 500,
- 'msg': '系统错误,请稍后重试',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def _get_game_type_name(self, leixing_id):
- try:
- from shangpin.models import ShangpinLeixing
- gt = ShangpinLeixing.objects.filter(id=leixing_id).first()
- return gt.jieshao if gt else '游戏订单'
- except Exception as e:
- logger.error(f"获取游戏类型名称异常: {e}")
- return '游戏订单'
-
- def send_order_notification_async(self, dingdan, youxi_nicheng, zhiding_dashou, zhuangtai):
- """异步发送订单通知(客户填写订单后)"""
- try:
- # 避免阻塞主线程,使用线程异步执行
- def notification_task():
- try:
- # 获取游戏类型名称
- try:
- leixing_obj = ShangpinLeixing.objects.get(id=dingdan.leixing_id)
- game_type = leixing_obj.jieshao
- except:
- game_type = '未知游戏'
-
- # 构建订单信息
- order_info = {
- 'dingdan_id': dingdan.dingdan_id,
- 'game_type': game_type,
- 'amount': str(dingdan.jine),
- 'order_desc': dingdan.jieshao[:50] if dingdan.jieshao else '新订单',
- 'youxi_nicheng': youxi_nicheng,
- 'order_type': '指定订单' if zhuangtai == 7 else '普通订单',
- 'zhiding_dashou': zhiding_dashou or ''
- }
-
- # 调用微信广播发送器
- sender = WeixinBroadcastSender()
- result = sender.broadcast_order(order_info)
-
- if result['success']:
- logger.info(f"【客户填写订单】微信通知发送成功: {result['msg']}")
- else:
- logger.warning(f"【客户填写订单】微信通知发送失败: {result['msg']}")
-
- except Exception as e:
- logger.error(f"【客户填写订单】发送通知任务异常: {str(e)}")
-
- # 启动异步线程
- thread = threading.Thread(target=notification_task)
- thread.daemon = True
- thread.start()
-
- except Exception as e:
- logger.error(f"【客户填写订单】启动通知线程异常: {str(e)}")
-
-
-
-
-
-'''class KehuTianxieDingdanView(APIView):
- """
- 客户填写订单信息接口
- 路径:POST /peizhi/kehutx
- """
-
- # 添加频率限制(比查询接口更严格)
- throttle_classes = [OrderLinkThrottle]
- permission_classes = [] # 无需认证
-
- def post(self, request):
- """
- 处理POST请求,填写订单信息
- """
- # 1. 记录请求开始
- logger.info("=== 客户填写订单信息接口开始 ===")
-
- # 2. 获取客户端真实IP
- client_ip = IPUtils.get_client_ip(request)
- logger.info(f"客户端IP: {client_ip}")
-
- # 3. 验证IP合法性
- if not IPUtils.validate_ip(client_ip):
- logger.warning(f"IP格式不合法: {client_ip}")
- return Response({
- 'code': 400,
- 'msg': '网络连接异常',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 获取请求参数
- data = request.data
- token = data.get('token', '')
- youxi_nicheng = data.get('youxi_nicheng', '').strip()
- zhiding_dashou = data.get('zhiding_dashou', '').strip() # 可选
- beizhu = data.get('beizhu', '').strip() # 可选
-
- logger.info(
- f"请求参数 - Token: {token[:8]}..., 昵称: {youxi_nicheng[:10]}..., 指定打手: {zhiding_dashou}, 备注长度: {len(beizhu)}")
-
- # 5. 验证必填参数
- if not token:
- logger.warning(f"Token参数缺失 - IP: {client_ip}")
- return Response({
- 'code': 400,
- 'msg': '缺少链接参数',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not youxi_nicheng:
- logger.warning(f"游戏昵称为空 - IP: {client_ip}")
- return Response({
- 'code': 400,
- 'msg': '游戏昵称不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 6. 验证游戏昵称格式
- if len(youxi_nicheng) > 50:
- logger.warning(f"游戏昵称过长 - IP: {client_ip}, 长度: {len(youxi_nicheng)}")
- return Response({
- 'code': 400,
- 'msg': '游戏昵称不能超过50个字符',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 过滤危险字符
- danger_chars = ['<', '>', "'", '"', '&', ';', '|']
- for char in danger_chars:
- if char in youxi_nicheng:
- logger.warning(f"游戏昵称包含危险字符 - IP: {client_ip}, 字符: {char}")
- return Response({
- 'code': 400,
- 'msg': '昵称包含非法字符',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 7. 验证指定打手ID格式(如果提供)
- if zhiding_dashou and len(zhiding_dashou) > 32:
- logger.warning(f"指定打手ID过长 - IP: {client_ip}, 长度: {len(zhiding_dashou)}")
- return Response({
- 'code': 400,
- 'msg': '指定打手ID不能超过32个字符',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 8. 验证备注长度
- if len(beizhu) > 500:
- logger.warning(f"备注过长 - IP: {client_ip}, 长度: {len(beizhu)}")
- return Response({
- 'code': 400,
- 'msg': '备注不能超过500个字符',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 9. 查询链接记录
- try:
- lianjie_obj = ShangjiaLianjie.objects.get(lianjie_token=token)
- logger.info(
- f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}, 是否已使用: {lianjie_obj.is_used}")
- except ShangjiaLianjie.DoesNotExist:
- logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}")
- return Response({
- 'code': 404,
- 'msg': '链接不存在或已失效',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 10. 检查链接有效期
- now = timezone.now()
- if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now:
- logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}")
- return Response({
- 'code': 400,
- 'msg': '链接已过期',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 11. 检查链接是否已使用
- if lianjie_obj.is_used:
- logger.warning(f"链接已使用 - 链接ID: {lianjie_obj.id}, IP: {client_ip}")
- return Response({
- 'code': 400,
- 'msg': '链接已被使用,无法重复提交',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 12. 验证指定打手(如果需要)
- zhuangtai_xin = 1 # 默认普通订单状态
- if zhiding_dashou:
- # 验证打手是否存在且有效
- try:
- # 先查询用户主表
- dashou_user = UserMain.objects.get(yonghuid=zhiding_dashou)
- logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}")
-
- # 通过反向关系获取打手扩展信息
- try:
- dashou_profile = dashou_user.dashou_profile
-
- # 检查打手状态(zhuangtai)
- if dashou_profile.zhuangtai != 1:
- logger.warning(f"打手状态异常 - 打手ID: {zhiding_dashou}, 状态: {dashou_profile.zhuangtai}")
- return Response({
- 'code': 400,
- 'msg': '指定打手当前无法接单',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 检查打手账号状态(zhanghaozhuangtai)
- if dashou_profile.zhanghaozhuangtai != 1:
- logger.warning(
- f"打手账号状态异常 - 打手ID: {zhiding_dashou}, 账号状态: {dashou_profile.zhanghaozhuangtai}")
- return Response({
- 'code': 400,
- 'msg': '指定打手账号异常',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
-
-
- # 验证通过,设置为指定订单状态
- zhuangtai_xin = 7 # 指定打手订单状态
- logger.info(f"指定打手验证通过 - 打手ID: {zhiding_dashou}, 昵称: {dashou_profile.nicheng}")
-
- except UserDashou.DoesNotExist:
- logger.warning(f"打手扩展信息不存在 - 用户ID: {zhiding_dashou}")
- return Response({
- 'code': 400,
- 'msg': '指定打手信息不完整',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- except UserMain.DoesNotExist:
- logger.warning(f"指定打手用户不存在 - 用户ID: {zhiding_dashou}")
- return Response({
- 'code': 400,
- 'msg': '指定打手不存在',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 13. 使用事务确保数据一致性
- try:
- with transaction.atomic():
- # 更新链接记录
- lianjie_obj.shiyongzhe_ip = client_ip
- lianjie_obj.is_used = True
- lianjie_obj.save(update_fields=['shiyongzhe_ip', 'is_used'])
- logger.info(f"更新链接记录成功 - 链接ID: {lianjie_obj.id}, 设置为已使用")
-
- # 查询订单
- if not lianjie_obj.dingdan_id:
- logger.error(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}")
- raise ValueError("订单ID为空")
-
- dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id)
-
- # 检查订单状态是否为13(灰色状态)
- if dingdan_obj.zhuangtai != 13:
- logger.warning(
- f"订单状态不允许修改 - 订单ID: {dingdan_obj.dingdan_id}, 当前状态: {dingdan_obj.zhuangtai}")
- # 回滚事务
- transaction.set_rollback(True)
- return Response({
- 'code': 400,
- 'msg': '订单状态异常,无法修改',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 更新订单信息
- dingdan_obj.nicheng = youxi_nicheng # 游戏昵称
- dingdan_obj.beizhu = beizhu if beizhu else '' # 备注
- dingdan_obj.zhuangtai = zhuangtai_xin # 新状态:1或7
-
- # 如果是指定打手,设置zhiding_id
- if zhiding_dashou and zhuangtai_xin == 7:
- dingdan_obj.zhiding_id = zhiding_dashou
-
- dingdan_obj.save()
- logger.info(
- f"更新订单成功 - 订单ID: {dingdan_obj.dingdan_id}, 新状态: {zhuangtai_xin}, 指定打手: {zhiding_dashou or '无'}")
-
- # 🔴【添加】在订单更新成功后,异步发送微信服务号通知
- self.send_order_notification_async(dingdan_obj, youxi_nicheng, zhiding_dashou, zhuangtai_xin)
-
- # 获取商家信息用于返回
- try:
- user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id)
- shangjia_obj = user_main.shop_profile
- shangjia_mingcheng = shangjia_obj.nicheng
- except:
- shangjia_mingcheng = f"商家{lianjie_obj.yonghu_id}"
- logger.warning(f"获取商家昵称失败,使用默认 - 用户ID: {lianjie_obj.yonghu_id}")
-
- # 14. 返回成功响应
- response_data = {
- #'dingdan_id': dingdan_obj.dingdan_id,
- #'create_time': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S'),
- #'shangpin_mingcheng': dingdan_obj.jieshao[:50] if dingdan_obj.jieshao else '', # 截取前50字符
- #'jiage': str(dingdan_obj.jine) if dingdan_obj.jine else '0.00',
- 'shangjia_mingcheng': shangjia_mingcheng,
- 'youxi_nicheng': youxi_nicheng,
- 'zhuangtai': zhuangtai_xin,
- 'zhuangtai_text': '普通订单' if zhuangtai_xin == 1 else '指定打手订单'
- }
-
- logger.info(f"订单提交成功 - 订单ID: {dingdan_obj.dingdan_id}, IP: {client_ip}")
-
- return Response({
- 'code': 200,
- 'msg': '提交成功',
- 'data': response_data
- }, status=status.HTTP_200_OK)
-
- except Dingdan.DoesNotExist:
- logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}")
- return Response({
- 'code': 500,
- 'msg': '订单数据异常',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- except Exception as e:
- logger.error(f"处理订单异常: {str(e)}", exc_info=True)
- return Response({
- 'code': 500,
- 'msg': '系统错误,请稍后重试',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def send_order_notification_async(self, dingdan, youxi_nicheng, zhiding_dashou, zhuangtai):
- """异步发送订单通知(客户填写订单后)"""
- try:
- # 避免阻塞主线程,使用线程异步执行
- def notification_task():
- try:
- # 获取游戏类型名称
- try:
- leixing_obj = ShangpinLeixing.objects.get(id=dingdan.leixing_id)
- game_type = leixing_obj.jieshao
- except:
- game_type = '未知游戏'
-
- # 构建订单信息
- order_info = {
- 'dingdan_id': dingdan.dingdan_id,
- 'game_type': game_type,
- 'amount': str(dingdan.jine),
- 'order_desc': dingdan.jieshao[:50] if dingdan.jieshao else '新订单',
- 'youxi_nicheng': youxi_nicheng,
- 'order_type': '指定订单' if zhuangtai == 7 else '普通订单',
- 'zhiding_dashou': zhiding_dashou or ''
- }
-
- # 调用微信广播发送器
- sender = WeixinBroadcastSender()
- result = sender.broadcast_order(order_info)
-
- if result['success']:
- logger.info(f"【客户填写订单】微信通知发送成功: {result['msg']}")
- else:
- logger.warning(f"【客户填写订单】微信通知发送失败: {result['msg']}")
-
- except Exception as e:
- logger.error(f"【客户填写订单】发送通知任务异常: {str(e)}")
-
- # 启动异步线程
- thread = threading.Thread(target=notification_task)
- thread.daemon = True
- thread.start()
-
- except Exception as e:
- logger.error(f"【客户填写订单】启动通知线程异常: {str(e)}")'''
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-from django.db.models import Max, F
-
-
-class GuanZhuAListView(APIView):
- """
- 获取关注阿龙页面图片列表
- POST /peizhi/gzal
- 需要JWT认证
- 返回:
- {
- "code": 0,
- "msg": "success",
- "data": {
- "images": ["相对URL1", "相对URL2", ...],
- "last_update": "2025-03-16 12:34:56",
- "visit_count": 123
- }
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 查询类型为3的所有记录
- qs = Tupianpeizhi.objects.filter(leixing=3)
- if not qs.exists():
- return Response({
- 'code': 0,
- 'msg': 'success',
- 'data': {
- 'images': [],
- 'last_update': None,
- 'visit_count': 0
- }
- }, status=status.HTTP_200_OK)
-
- # 获取所有图片的相对URL
- images = list(qs.values_list('tupian', flat=True))
-
- # 获取最新更新时间(取所有记录的update_time最大值)
- last_update = qs.aggregate(Max('update_time'))['update_time__max']
- last_update_str = last_update.strftime('%Y-%m-%d %H:%M:%S') if last_update else None
-
- # 原子性增加访问次数
- # 先更新所有记录,再获取更新后的最大次数
- qs.update(fangwen_cishu=F('fangwen_cishu') + 1)
- visit_count = qs.aggregate(Max('fangwen_cishu'))['fangwen_cishu__max'] or 0
-
- return Response({
- 'code': 0,
- 'msg': 'success',
- 'data': {
- 'images': images,
- 'last_update': last_update_str,
- 'visit_count': visit_count
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f"获取关注阿龙图片列表失败: {e}", exc_info=True)
- return Response({
- 'code': 1,
- 'msg': '服务器内部错误',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-
-# views.py
-import io
-import time
-
-from yonghu.models import UserGuanshi
-from utils.oss_utils import upload_to_oss, delete_from_oss
-from utils.yaoqingma_utils import chuangjianYaoqingma, yanzhengYaoqingmaWeiyixing
-import requests
-
-logger = logging.getLogger(__name__)
-
-
-
-import urllib.parse
-
-
-#不用缓存,一天2000次
-class GuanshiQRCodeView(APIView):
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- logger.info("===== 开始生成管事二维码 =====")
- current_user = request.user
- logger.info(f"当前用户ID: {current_user.yonghuid}")
-
- try:
- try:
- guanshi = current_user.guanshi_profile
- logger.info(f"管事扩展表查询成功,状态: {guanshi.zhuangtai}")
- except UserGuanshi.DoesNotExist:
- logger.warning(f"用户 {current_user.yonghuid} 不是管事身份")
- return Response(
- {'code': 403, 'message': '当前用户不是管事身份', 'data': None},
- status=status.HTTP_403_FORBIDDEN
- )
-
- if guanshi.zhuangtai != 1:
- logger.warning(f"用户 {current_user.yonghuid} 管事状态异常: {guanshi.zhuangtai}")
- return Response(
- {'code': 403, 'message': '管事账号状态异常', 'data': None},
- status=status.HTTP_403_FORBIDDEN
- )
-
- invite_code = guanshi.yaoqingma
- if not invite_code:
- logger.info(f"用户 {current_user.yonghuid} 无邀请码,开始生成")
- invite_code = chuangjianYaoqingma(str(current_user.yonghuid))
- if not yanzhengYaoqingmaWeiyixing(invite_code):
- logger.error(f"邀请码唯一性校验失败,用户: {current_user.yonghuid}")
- return Response(
- {'code': 500, 'message': '邀请码生成失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
- guanshi.yaoqingma = invite_code
- guanshi.save(update_fields=['yaoqingma'])
- logger.info(f"邀请码生成成功: {invite_code}")
- else:
- logger.info(f"使用已有邀请码: {invite_code}")
-
- # 直接获取新的 access_token,不缓存
- access_token = self.get_fresh_access_token()
- if not access_token:
- logger.error("获取 access_token 失败")
- return Response(
- {'code': 500, 'message': '获取微信access_token失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
- logger.info(f"获取到 access_token (前10位): {access_token[:10]}...")
-
- encoded_invite = urllib.parse.quote(invite_code, safe='')
- page_path = f'pages/dashouduan/dashouduan?inviteCode={encoded_invite}'
- wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
- post_data = {
- 'path': page_path,
- 'width': 430,
- 'auto_color': False,
- 'line_color': {'r': 0, 'g': 0, 'b': 0}
- }
- logger.info(f"调用微信接口生成小程序码,path: {page_path}")
-
- try:
- wx_resp = requests.post(wx_url, json=post_data, timeout=10)
- logger.info(f"微信接口响应状态码: {wx_resp.status_code}")
- except Exception as e:
- logger.error(f"请求微信接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '调用微信服务失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- if wx_resp.status_code != 200:
- error_info = {}
- if wx_resp.headers.get('content-type') == 'application/json':
- try:
- error_info = wx_resp.json()
- except:
- pass
- errmsg = error_info.get('errmsg', '未知错误')
- logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}")
- if 'errcode' in error_info:
- logger.error(f"微信错误码: {error_info.get('errcode')}")
- return Response(
- {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- content_type = wx_resp.headers.get('content-type', '')
- if 'image' not in content_type:
- try:
- error_info = wx_resp.json()
- errmsg = error_info.get('errmsg', '未知错误')
- logger.error(f"微信接口返回非图片内容: {errmsg}")
- return Response(
- {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
- except:
- logger.error("微信接口返回未知内容,无法解析为JSON")
- return Response(
- {'code': 500, 'message': '生成二维码失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- timestamp = int(time.time())
- filename = f"{current_user.yonghuid}_{timestamp}.png"
- oss_path = f"erweima/guanshi/{filename}"
- file_obj = io.BytesIO(wx_resp.content)
- logger.info(f"开始上传图片到COS: {oss_path}")
-
- try:
- full_url = upload_to_oss(file_obj, oss_path)
- logger.info(f"上传成功,完整URL: {full_url}")
- except Exception as e:
- logger.error(f"上传到COS异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '图片上传失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- if not full_url:
- logger.error("upload_to_oss 返回了空 URL")
- return Response(
- {'code': 500, 'message': '图片上传失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- cos_domain = getattr(settings, 'COS_DOMAIN', '')
- if cos_domain and full_url.startswith(cos_domain):
- relative_path = full_url[len(cos_domain):].lstrip('/')
- else:
- relative_path = oss_path
- logger.info(f"相对路径: {relative_path}")
-
- old_url = guanshi.invite_qrcode_url
- if old_url:
- logger.info(f"删除旧二维码: {old_url}")
- try:
- delete_from_oss(old_url)
- except Exception as e:
- logger.warning(f"删除旧二维码失败(不影响主流程): {str(e)}")
-
- with transaction.atomic():
- guanshi.invite_qrcode_url = relative_path
- guanshi.save(update_fields=['invite_qrcode_url'])
- logger.info("数据库更新成功")
-
- return Response({
- 'code': 0,
- 'message': 'success',
- 'data': {'url': relative_path}
- })
-
- except Exception as e:
- logger.error(f"生成二维码过程中发生未捕获异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- def get_fresh_access_token(self):
- """
- 直接请求微信接口获取新的 access_token,不缓存,保证最新。
- """
- appid = getattr(settings, 'WEIXIN_APPID', '')
- secret = getattr(settings, 'WEIXIN_SECRET', '')
- if not appid or not secret:
- logger.error("微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置")
- return None
-
- url = 'https://api.weixin.qq.com/cgi-bin/token'
- params = {
- 'grant_type': 'client_credential',
- 'appid': appid,
- 'secret': secret
- }
- try:
- logger.info("正在请求微信接口获取最新 access_token...")
- resp = requests.get(url, params=params, timeout=5)
- data = resp.json()
- new_token = data.get('access_token')
- if new_token:
- logger.info("成功获取最新 access_token")
- return new_token
- else:
- errcode = data.get('errcode', '未知')
- errmsg = data.get('errmsg', '未知错误')
- logger.error(f"获取 access_token 失败,errcode={errcode}, errmsg={errmsg}")
- return None
- except Exception as e:
- logger.error(f"请求微信 access_token 接口异常: {str(e)}", exc_info=True)
- return None
-
-#用了缓存
-'''class GuanshiQRCodeView(APIView):
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- logger.info("===== 开始生成管事二维码 =====")
- current_user = request.user
- logger.info(f"当前用户ID: {current_user.yonghuid}")
-
- try:
- try:
- guanshi = current_user.guanshi_profile
- logger.info(f"管事扩展表查询成功,状态: {guanshi.zhuangtai}")
- except UserGuanshi.DoesNotExist:
- logger.warning(f"用户 {current_user.yonghuid} 不是管事身份")
- return Response(
- {'code': 403, 'message': '当前用户不是管事身份', 'data': None},
- status=status.HTTP_403_FORBIDDEN
- )
-
- if guanshi.zhuangtai != 1:
- logger.warning(f"用户 {current_user.yonghuid} 管事状态异常: {guanshi.zhuangtai}")
- return Response(
- {'code': 403, 'message': '管事账号状态异常', 'data': None},
- status=status.HTTP_403_FORBIDDEN
- )
-
- invite_code = guanshi.yaoqingma
- if not invite_code:
- logger.info(f"用户 {current_user.yonghuid} 无邀请码,开始生成")
- invite_code = chuangjianYaoqingma(str(current_user.yonghuid))
- if not yanzhengYaoqingmaWeiyixing(invite_code):
- logger.error(f"邀请码唯一性校验失败,用户: {current_user.yonghuid}")
- return Response(
- {'code': 500, 'message': '邀请码生成失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
- guanshi.yaoqingma = invite_code
- guanshi.save(update_fields=['yaoqingma'])
- logger.info(f"邀请码生成成功: {invite_code}")
- else:
- logger.info(f"使用已有邀请码: {invite_code}")
-
- access_token = self.get_cached_access_token()
- if not access_token:
- logger.error("获取 access_token 失败")
- return Response(
- {'code': 500, 'message': '获取微信access_token失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
- logger.info(f"获取到 access_token (前10位): {access_token[:10]}...")
-
- encoded_invite = urllib.parse.quote(invite_code, safe='')
- page_path = f'pages/dashouduan/dashouduan?inviteCode={encoded_invite}'
- wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
- post_data = {
- 'path': page_path,
- 'width': 430,
- 'auto_color': False,
- 'line_color': {'r': 0, 'g': 0, 'b': 0}
- }
- logger.info(f"调用微信接口生成小程序码,path: {page_path}")
-
- try:
- wx_resp = requests.post(wx_url, json=post_data, timeout=10)
- logger.info(f"微信接口响应状态码: {wx_resp.status_code}")
- except Exception as e:
- logger.error(f"请求微信接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '调用微信服务失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- if wx_resp.status_code != 200:
- error_info = {}
- if wx_resp.headers.get('content-type') == 'application/json':
- try:
- error_info = wx_resp.json()
- except:
- pass
- errmsg = error_info.get('errmsg', '未知错误')
- logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}")
- if 'errcode' in error_info:
- logger.error(f"微信错误码: {error_info.get('errcode')}")
- return Response(
- {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- content_type = wx_resp.headers.get('content-type', '')
- if 'image' not in content_type:
- try:
- error_info = wx_resp.json()
- errmsg = error_info.get('errmsg', '未知错误')
- logger.error(f"微信接口返回非图片内容: {errmsg}")
- return Response(
- {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
- except:
- logger.error("微信接口返回未知内容,无法解析为JSON")
- return Response(
- {'code': 500, 'message': '生成二维码失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- timestamp = int(time.time())
- filename = f"{current_user.yonghuid}_{timestamp}.png"
- oss_path = f"erweima/guanshi/{filename}"
- file_obj = io.BytesIO(wx_resp.content)
- logger.info(f"开始上传图片到COS: {oss_path}")
-
- try:
- full_url = upload_to_oss(file_obj, oss_path)
- logger.info(f"上传成功,完整URL: {full_url}")
- except Exception as e:
- logger.error(f"上传到COS异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '图片上传失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- if not full_url:
- logger.error("upload_to_oss 返回了空 URL")
- return Response(
- {'code': 500, 'message': '图片上传失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- cos_domain = getattr(settings, 'COS_DOMAIN', '')
- if cos_domain and full_url.startswith(cos_domain):
- relative_path = full_url[len(cos_domain):].lstrip('/')
- else:
- relative_path = oss_path
- logger.info(f"相对路径: {relative_path}")
-
- old_url = guanshi.invite_qrcode_url
- if old_url:
- logger.info(f"删除旧二维码: {old_url}")
- try:
- delete_from_oss(old_url)
- except Exception as e:
- logger.warning(f"删除旧二维码失败(不影响主流程): {str(e)}")
-
- with transaction.atomic():
- guanshi.invite_qrcode_url = relative_path
- guanshi.save(update_fields=['invite_qrcode_url'])
- logger.info("数据库更新成功")
-
- return Response({
- 'code': 0,
- 'message': 'success',
- 'data': {'url': relative_path}
- })
-
- except Exception as e:
- logger.error(f"生成二维码过程中发生未捕获异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- def get_cached_access_token(self):
- """获取微信全局 access_token,使用 cache.add 实现互斥锁"""
- cache_key = 'wechat_access_token'
- lock_key = 'wechat_access_token_lock'
- lock_timeout = 10
-
- # 先尝试读取缓存
- token = cache.get(cache_key)
- if token:
- logger.info("从缓存中读取到 access_token")
- return token
-
- # 尝试获取锁(原子操作)
- lock_acquired = cache.add(lock_key, '1', timeout=lock_timeout)
- if not lock_acquired:
- # 未获得锁,等待其他进程刷新
- import time
- time.sleep(0.2)
- token = cache.get(cache_key)
- if token:
- logger.info("其他进程已刷新 token,使用缓存值")
- return token
- # 等待几秒再试
- for _ in range(5):
- time.sleep(0.2)
- token = cache.get(cache_key)
- if token:
- return token
- logger.warning("获取锁失败且缓存中无 token,返回 None")
- return None
-
- # 获得锁,执行刷新
- try:
- token = cache.get(cache_key)
- if token:
- logger.info("加锁后再次读取到缓存中的 token")
- return token
-
- appid = getattr(settings, 'WEIXIN_APPID', '')
- secret = getattr(settings, 'WEIXIN_SECRET', '')
- if not appid or not secret:
- logger.error("微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置")
- return None
-
- url = 'https://api.weixin.qq.com/cgi-bin/token'
- params = {
- 'grant_type': 'client_credential',
- 'appid': appid,
- 'secret': secret
- }
- try:
- logger.info("正在请求微信接口获取 access_token...")
- resp = requests.get(url, params=params, timeout=5)
- data = resp.json()
- new_token = data.get('access_token')
- if new_token:
- cache.set(cache_key, new_token, timeout=7100)
- logger.info("成功获取新 access_token,已缓存")
- return new_token
- else:
- errcode = data.get('errcode', '未知')
- errmsg = data.get('errmsg', '未知错误')
- logger.error(f"获取 access_token 失败,errcode={errcode}, errmsg={errmsg}")
- return None
- except Exception as e:
- logger.error(f"请求微信 access_token 接口异常: {str(e)}", exc_info=True)
- return None
- finally:
- cache.delete(lock_key)'''
- # ==================== 修改结束 ====================
-
-
-
-
-from yonghu.models import UserZuzhang
-
-
-
-
-
-
-
-logger = logging.getLogger(__name__)
-
-
-class ZuzhangHaibaoView(APIView):
- """
- 组长专属海报二维码生成接口
- 路径:/peizhi/zuzhanghb
- 方法:POST
- 权限:JWT认证,仅限组长身份
- 返回:
- {
- "code": 0,
- "message": "success",
- "data": {
- "url": "erweima/zuzhang/1000001_1234567890.png",
- "yaoqingma": "ABC123XYZ"
- }
- }
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- current_user = request.user
-
- # 1. 验证组长身份
- try:
- zuzhang = current_user.zuzhang_profile
- except UserZuzhang.DoesNotExist:
- return Response({
- 'code': 403,
- 'message': '当前用户不是组长身份',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 2. 验证组长账号状态
- if zuzhang.zhuangtai != 1:
- return Response({
- 'code': 403,
- 'message': '组长账号状态异常,无法生成海报',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 3. 获取或生成邀请码
- invite_code = zuzhang.yaoqingma
- if not invite_code:
- invite_code = chuangjianYaoqingma(str(current_user.yonghuid))
- if not yanzhengYaoqingmaWeiyixing(invite_code):
- logger.error(f"组长邀请码生成失败,用户: {current_user.yonghuid}")
- return Response({
- 'code': 500,
- 'message': '邀请码生成失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
- zuzhang.yaoqingma = invite_code
- zuzhang.save(update_fields=['yaoqingma'])
-
- # 4. 获取微信 access_token
- access_token = self.get_cached_access_token()
- if not access_token:
- return Response({
- 'code': 500,
- 'message': '获取微信access_token失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # ===== 修改点:使用 A 接口(getwxacode) =====
- # 对邀请码进行 URL 编码,防止特殊字符破坏路径
- encoded_invite = urllib.parse.quote(invite_code, safe='')
- # 构造带参数的完整路径(请根据实际页面调整)
- page_path = f'pages/guanshiduan/guanshiduan?inviteCode={encoded_invite}'
-
- wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
- post_data = {
- 'path': page_path,
- 'width': 430,
- 'auto_color': False,
- 'line_color': {'r': 0, 'g': 0, 'b': 0}
- }
-
- try:
- wx_resp = requests.post(wx_url, json=post_data, timeout=10)
- except Exception as e:
- logger.error(f"请求微信接口失败: {e}")
- return Response({
- 'code': 500,
- 'message': '调用微信服务失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 处理微信返回结果
- if wx_resp.status_code != 200:
- error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {}
- errmsg = error_info.get('errmsg', '未知错误')
- logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}")
- return Response({
- 'code': 500,
- 'message': f'生成二维码失败: {errmsg}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- content_type = wx_resp.headers.get('content-type', '')
- if 'image' not in content_type:
- try:
- error_info = wx_resp.json()
- errmsg = error_info.get('errmsg', '未知错误')
- logger.error(f"微信接口返回非图片内容: {errmsg}")
- return Response({
- 'code': 500,
- 'message': f'生成二维码失败: {errmsg}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
- except:
- logger.error("微信接口返回未知内容")
- return Response({
- 'code': 500,
- 'message': '生成二维码失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 5. 上传到腾讯云 COS
- timestamp = int(time.time())
- filename = f"{current_user.yonghuid}_{timestamp}.png"
- oss_path = f"erweima/zuzhang/{filename}"
- file_obj = io.BytesIO(wx_resp.content)
-
- try:
- full_url = upload_to_oss(file_obj, oss_path)
- except Exception as e:
- logger.error(f"上传到COS失败: {e}")
- return Response({
- 'code': 500,
- 'message': '图片上传失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- if not full_url:
- return Response({
- 'code': 500,
- 'message': '图片上传失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 6. 获取相对路径
- cos_domain = getattr(settings, 'COS_DOMAIN', '')
- if cos_domain and full_url.startswith(cos_domain):
- relative_path = full_url[len(cos_domain):].lstrip('/')
- else:
- relative_path = oss_path
-
- # 7. 删除旧二维码文件
- old_url = zuzhang.haibao_url
- if old_url:
- delete_from_oss(old_url)
-
- # 8. 更新数据库中的 haibao_url 字段
- with transaction.atomic():
- zuzhang.haibao_url = relative_path
- zuzhang.save(update_fields=['haibao_url'])
-
- # 9. 返回邀请码和二维码相对路径
- return Response({
- 'code': 0,
- 'message': 'success',
- 'data': {
- 'url': relative_path,
- 'yaoqingma': invite_code
- }
- })
-
- def get_cached_access_token(self):
- """从缓存获取微信 access_token"""
- cache_key = 'wx_mini_access_token'
- token = cache.get(cache_key)
- if token:
- return token
-
- appid = getattr(settings, 'WEIXIN_APPID', '')
- secret = getattr(settings, 'WEIXIN_SECRET', '')
- if not appid or not secret:
- logger.error("微信小程序配置缺失")
- return None
-
- url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}'
- try:
- resp = requests.get(url, timeout=5)
- data = resp.json()
- token = data.get('access_token')
- expires_in = data.get('expires_in', 7200)
- if token:
- cache.set(cache_key, token, 7000)
- return token
- except Exception as e:
- logger.error(f"获取微信access_token失败: {e}")
- return None
-'''class ZuzhangHaibaoView(APIView):
- """
- 组长专属海报二维码生成接口
- 路径:/peizhi/zuzhanghb
- 方法:POST
- 权限:JWT认证,仅限组长身份
- 返回:
- {
- "code": 0,
- "message": "success",
- "data": {
- "url": "erweima/zuzhang/1000001_1234567890.png", # 相对路径
- "yaoqingma": "ABC123XYZ" # 组长邀请码
- }
- }
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- current_user = request.user
-
- # 1. 验证组长身份
- try:
- zuzhang = current_user.zuzhang_profile
- except UserZuzhang.DoesNotExist:
- return Response({
- 'code': 403,
- 'message': '当前用户不是组长身份',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 2. 验证组长账号状态(zhuangtai=1正常)
- if zuzhang.zhuangtai != 1:
- return Response({
- 'code': 403,
- 'message': '组长账号状态异常,无法生成海报',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 3. 获取或生成邀请码(yaoqingma字段)
- invite_code = zuzhang.yaoqingma
- if not invite_code:
- # 生成唯一邀请码(使用已有工具函数)
- invite_code = chuangjianYaoqingma(str(current_user.yonghuid))
- if not yanzhengYaoqingmaWeiyixing(invite_code):
- logger.error(f"组长邀请码生成失败,用户: {current_user.yonghuid}")
- return Response({
- 'code': 500,
- 'message': '邀请码生成失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
- # 更新到数据库
- zuzhang.yaoqingma = invite_code
- zuzhang.save(update_fields=['yaoqingma'])
-
- # 4. 获取微信 access_token(使用缓存)
- access_token = self.get_cached_access_token()
- if not access_token:
- return Response({
- 'code': 500,
- 'message': '获取微信access_token失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # ========== 调用微信B接口:getUnlimited ==========
- # 页面路径:打手注册页(组长邀请打手)
- page_path = 'pages/guanshiduan/guanshiduan'
- # scene参数:直接放入邀请码(需确保字符符合微信要求)
- scene = invite_code # 假设邀请码只包含字母数字
-
- wx_url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}'
- post_data = {
- 'scene': scene,
- 'page': page_path,
- 'width': 430,
- 'auto_color': False,
- 'line_color': {'r': 0, 'g': 0, 'b': 0}
- }
-
- try:
- wx_resp = requests.post(wx_url, json=post_data, timeout=10)
- except Exception as e:
- logger.error(f"请求微信接口失败: {e}")
- return Response({
- 'code': 500,
- 'message': '调用微信服务失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 处理微信返回结果
- if wx_resp.status_code != 200:
- error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {}
- errmsg = error_info.get('errmsg', '未知错误')
- logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}")
- return Response({
- 'code': 500,
- 'message': f'生成二维码失败: {errmsg}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- content_type = wx_resp.headers.get('content-type', '')
- if 'image' not in content_type:
- # 可能是JSON错误
- try:
- error_info = wx_resp.json()
- errmsg = error_info.get('errmsg', '未知错误')
- logger.error(f"微信接口返回非图片内容: {errmsg}")
- return Response({
- 'code': 500,
- 'message': f'生成二维码失败: {errmsg}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
- except:
- logger.error("微信接口返回未知内容")
- return Response({
- 'code': 500,
- 'message': '生成二维码失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 5. 上传二维码到腾讯云COS
- timestamp = int(time.time())
- filename = f"{current_user.yonghuid}_{timestamp}.png"
- oss_path = f"erweima/zuzhang/{filename}" # 组长专用文件夹
- file_obj = io.BytesIO(wx_resp.content)
-
- try:
- full_url = upload_to_oss(file_obj, oss_path)
- except Exception as e:
- logger.error(f"上传到COS失败: {e}")
- return Response({
- 'code': 500,
- 'message': '图片上传失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- if not full_url:
- return Response({
- 'code': 500,
- 'message': '图片上传失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 6. 获取相对路径(用于前端拼接)
- cos_domain = getattr(settings, 'COS_DOMAIN', '')
- if cos_domain and full_url.startswith(cos_domain):
- relative_path = full_url[len(cos_domain):].lstrip('/')
- else:
- relative_path = oss_path
-
- # 7. 删除旧的二维码文件(haibao_url字段)
- old_url = zuzhang.haibao_url
- if old_url:
- delete_from_oss(old_url)
-
- # 8. 更新数据库中的haibao_url字段
- with transaction.atomic():
- zuzhang.haibao_url = relative_path
- zuzhang.save(update_fields=['haibao_url'])
-
- # 9. 返回邀请码和二维码相对路径
- return Response({
- 'code': 0,
- 'message': 'success',
- 'data': {
- 'url': relative_path,
- 'yaoqingma': invite_code
- }
- })
-
- def get_cached_access_token(self):
- """从缓存获取微信access_token(与管事接口共用)"""
- cache_key = 'wx_mini_access_token'
- token = cache.get(cache_key)
- if token:
- return token
-
- appid = getattr(settings, 'WEIXIN_APPID', '')
- secret = getattr(settings, 'WEIXIN_SECRET', '')
- if not appid or not secret:
- logger.error("微信小程序配置缺失")
- return None
-
- url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}'
- try:
- resp = requests.get(url, timeout=5)
- data = resp.json()
- token = data.get('access_token')
- expires_in = data.get('expires_in', 7200)
- if token:
- cache.set(cache_key, token, 7000)
- return token
- except Exception as e:
- logger.error(f"获取微信access_token失败: {e}")
- return None'''
-
-
-
-
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework import status
-from django.utils import timezone
-from django.db.models import Q
-from .models import PopupPage, PopupConfig
-from .serializers import PopupConfigSerializer
-
-
-class PopupConfigView(APIView):
- """
- 获取指定页面的弹窗配置
- 请求格式:POST { "pageKey": "dashouzhongxin" }
- 返回格式:{ "code": 0, "data": { "serverTime": "...", "popups": [...] } }
- """
- permission_classes = [IsAuthenticated] # JWT 认证
-
- def post(self, request):
- # 1. 获取当前用户(已通过JWT认证,可直接使用)
- user = request.user
- if not user or not user.id:
- return Response({
- 'code': 401,
- 'msg': '用户未登录或认证失败'
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 获取参数 pageKey
- page_key = request.data.get('pageKey')
- if not page_key:
- return Response({
- 'code': 400,
- 'msg': '缺少参数 pageKey'
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 查询页面配置
- try:
- popup_page = PopupPage.objects.get(page_key=page_key, is_active=True)
- except PopupPage.DoesNotExist:
- # 该页面没有配置弹窗,返回空
- return Response({
- 'code': 0,
- 'data': {
- 'serverTime': timezone.now().isoformat(),
- 'popups': []
- }
- })
-
- # 4. 查询该页面下所有启用的弹窗(且在有效时间范围内)
- now = timezone.now()
- popups = PopupConfig.objects.filter(
- page=popup_page,
- is_active=True,
- start_time__lte=now,
- end_time__gte=now
- ).prefetch_related('images') # 预加载图片,避免N+1查询
- # 按 sort_order 排序(model Meta 已定义)
-
- # 5. 序列化
- serializer = PopupConfigSerializer(popups, many=True)
-
- # 6. 返回数据
- return Response({
- 'code': 0,
- 'data': {
- 'serverTime': now.isoformat(),
- 'ignore_user_mute': popup_page.ignore_user_mute, # 新增
- 'popups': serializer.data
- }
- })
-
-
-from .models import WithdrawConfig
-
-class GetWithdrawModeView(APIView):
- """
- 获取提现模式配置
- 前端请求 /peizhi/hqtxzsym
- 返回格式: { "code": 0, "data": { "mode": 1 or 2 } }
- """
- permission_classes = [IsAuthenticated] # JWT 认证
-
- def post(self, request, *args, **kwargs):
- # 尝试获取 ID=1 的记录
- config = WithdrawConfig.objects.filter(id=1).first()
- # 如果存在则取实际值,否则默认 2(手动提现)
- mode = config.mode if config else 2
-
- return Response({
- 'code': 0,
- 'data': {
- 'mode': mode
- }
- })
-
-
-
-
-from shangpin.models import (
- Huiyuangoumai
-)
-
-
-class CheckPhoneAuthView(APIView):
- """
- 判断当前登录用户是否需要强制手机号认证
-
- 规则:
- - 已认证过手机号(shoujihao_renzheng=True) → 不需要
- - 未认证时,检查用户角色及其业务指标是否达到阈值:
- 打手:有会员购买记录(Huiyuangoumai 中存在记录)
- 商家:发布订单总数(fabu)>= 5
- 管事:充值分佣总额(chongzhifenrun)>= 15
- 组长:分佣总额(fenyong_zonge)>= 2
- 老板:消费总额(zonge)>= 20
- - 满足任一角色的阈值 → 需要认证
- - 否则 → 不需要认证
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- user = request.user # UserMain 实例
-
- # 已经认证过手机号,直接放行
- if user.shoujihao_renzheng:
- return Response({'need_auth': False})
-
- # 检查各角色是否满足阈值条件
- if self._dashou_meets_threshold(user):
- return Response({'need_auth': True})
- if self._shangjia_meets_threshold(user):
- return Response({'need_auth': True})
- if self._guanshi_meets_threshold(user):
- return Response({'need_auth': True})
- if self._zuzhang_meets_threshold(user):
- return Response({'need_auth': True})
- if self._boss_meets_threshold(user):
- return Response({'need_auth': True})
-
- # 都不满足,无需认证
- return Response({'need_auth': False})
-
- # ==================== 各角色阈值判断方法 ====================
-
- def _dashou_meets_threshold(self, user):
- """
- 打手条件:存在会员购买记录(Huiyuangoumai 表中有该用户的记录)
- """
- try:
- # 先确认用户是打手
- UserDashou.objects.get(user=user)
- except UserDashou.DoesNotExist:
- return False
- # 检查是否有任意会员购买记录
- return Huiyuangoumai.objects.filter(yonghu_id=user.yonghuid).exists()
-
- def _shangjia_meets_threshold(self, user):
- """商家条件:发布订单总数 fabu >= 5"""
- try:
- shop = UserShangjia.objects.get(user=user)
- except UserShangjia.DoesNotExist:
- return False
- return shop.fabu >= 5
-
- def _guanshi_meets_threshold(self, user):
- """管事条件:充值分佣总额 chongzhifenrun >= 15"""
- try:
- guanshi = UserGuanshi.objects.get(user=user)
- except UserGuanshi.DoesNotExist:
- return False
- return guanshi.chongzhifenrun >= 15
-
- def _zuzhang_meets_threshold(self, user):
- """组长条件:分佣总额 fenyong_zonge >= 2"""
- try:
- zuzhang = UserZuzhang.objects.get(user=user)
- except UserZuzhang.DoesNotExist:
- return False
- return zuzhang.fenyong_zonge >= 2
-
- def _boss_meets_threshold(self, user):
- """老板条件:消费总额 zonge >= 20"""
- try:
- boss = UserBoss.objects.get(user=user)
- except UserBoss.DoesNotExist:
- return False
+import io
+import os
+import json
+import re
+import time
+import random
+import secrets
+import hashlib
+import threading
+import urllib.parse
+from decimal import Decimal, InvalidOperation
+
+from django.conf import settings
+from django.db import models, transaction, connection
+from django.db.models import Q, F, Max, Prefetch
+from django.core.cache import cache
+from django.core.paginator import Paginator
+from django.utils import timezone
+
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework import status, permissions
+from rest_framework.permissions import AllowAny, IsAuthenticated
+from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
+from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
+
+from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
+from utils.weixin_broadcast import WeixinBroadcastSender
+# from utils.ip_security import *
+from Django.utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
+
+from backend.utils import update_shangjia_daily
+from orders.utils import sync_order_to_partners
+
+from .models import (
+ Gonggao, Lunbo, ClubConfig, Tupianpeizhi, Qunpeizhi,
+ ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig
+)
+
+from users.models import (
+ UserMain, AdminProfile, UserDashou, UserBoss,
+ UserShangjia, UserGuanshi, UserZuzhang
+)
+
+from orders.models import Lilubiao, Dingdan, DingdanShangjia
+from orders.notice_tasks import dingdan_guangbo
+
+from rank.models import Chenghao, DingdanBiaoqian
+
+from products.models import ShangpinLeixing, Huiyuangoumai
+
+from .serializers import PopupConfigSerializer
+import traceback
+import requests
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+
+class GetDynamicConfigView(APIView):
+ permission_classes = [AllowAny]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 查询我方俱乐部配置(is_self=1),取第一条(通常只有一条)
+ club_config = ClubConfig.objects.filter(is_self=1).only(
+ 'storage_bucket_domain',
+ 'storage_bucket_name',
+ 'chat_api_key',
+ 'customer_service_api_url',
+ 'customer_service_corp_id'
+ ).first()
+
+ # 默认值(防止空数据导致前端报错)
+ cos_bucket = ''
+ cos_region = 'ap-shanghai'
+ cos_oss_url = ''
+ goeasy_host = 'hangzhou.goeasy.io'
+ goeasy_appkey = ''
+ kefu_link = ''
+ kefu_enterprise_id = ''
+ morentouxiang = 'beijing/morentouxiang.jpg'
+ dashouguize = 'a_long/dashouguize.jpg'
+
+ if club_config:
+ # 存储桶配置
+ bucket = club_config.storage_bucket_name or ''
+ region = 'ap-shanghai' # 若需要动态,可单独存储
+ cos_oss_url = club_config.storage_bucket_domain or ''
+ if cos_oss_url and not cos_oss_url.endswith('/'):
+ cos_oss_url += '/'
+ cos_bucket = bucket
+ cos_region = region
+
+ # GoEasy 配置
+ if club_config.chat_api_key:
+ goeasy_appkey = club_config.chat_api_key
+ # host 如果需要可动态,这里固定或从其他字段获取
+
+ # 客服配置
+ if club_config.customer_service_api_url:
+ kefu_link = club_config.customer_service_api_url
+ if club_config.customer_service_corp_id:
+ kefu_enterprise_id = club_config.customer_service_corp_id
+
+ # 其他配置可在此扩展
+
+ # 构造返回数据
+ data = {
+ "code": 0,
+ "data": {
+ "cos": {
+ "bucket": cos_bucket,
+ "region": cos_region,
+ "ossImageUrl": cos_oss_url,
+ "uploadPathPrefix": "order/"
+ },
+ "goEasy": {
+ "host": goeasy_host,
+ "appkey": goeasy_appkey
+ },
+ "otherConfig": {
+ "morentouxiang": morentouxiang,
+ "dashouguize": dashouguize
+ },
+ "kefu": {
+ "link": kefu_link,
+ "enterpriseId": kefu_enterprise_id
+ }
+ }
+ }
+ return Response(data)
+
+ except Exception as e:
+ logger.error(f"获取动态配置失败: {str(e)}", exc_info=True)
+ return Response({"code": 500, "msg": "服务器内部错误"}, status=500)
+
+
+
+
+class ShangpinGonggaoView(APIView):
+ """
+ 商品公告和轮播图接口
+ 获取类型为1的公告和轮播图
+ """
+ # 应用节流(限制频率)
+ throttle_classes = [AnonRateThrottle]
+ # 新增:允许所有用户访问(不需要认证)
+ permission_classes = [AllowAny]
+ def post(self, request):
+ """
+ 处理POST请求,返回公告和轮播图数据
+
+ 请求: POST /api/peizhi/shangpingonggao/
+ 返回: {
+ "shangpingonggao": "公告内容字符串",
+ "shangpinlunbo": ["轮播图URL1", "轮播图URL2"]
+ }
+ """
+ try:
+ logger.info("收到商品公告和轮播图请求")
+ # 1. 查询公告(类型为1)
+ gonggao_obj = Gonggao.objects.filter(leixing=1).first()
+ # 获取公告内容,如果没有则返回空字符串
+ gonggao_content = gonggao_obj.jieshao if gonggao_obj else ""
+ # 2. 查询轮播图(类型为1,按显示顺序排序)
+ lunbo_queryset = Lunbo.objects.filter(leixing=1)
+ # 提取轮播图URL列表
+ lunbo_urls = [lunbo.tupian_url for lunbo in lunbo_queryset]
+ # 3. 构建返回数据
+ response_data = {
+ "shangpingonggao": gonggao_content,
+ "shangpinlunbo": lunbo_urls
+ }
+ logger.info(f"返回数据:公告长度{len(gonggao_content)},轮播图数量{len(lunbo_urls)}")
+ # 4. 返回响应
+ return Response(response_data, status=status.HTTP_200_OK)
+ except Exception as e:
+ # 记录异常
+ logger.error(f"获取商品公告和轮播图时发生错误:{str(e)}")
+ # 返回错误响应
+ error_data = {
+ "error": "服务器内部错误",
+ "detail": "获取数据失败,请稍后重试"
+ }
+ return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+class AdminConfigQueryView(APIView):
+ """
+ 管理员获取系统配置接口
+ URL: /peizhi/adpzhq
+ 方法: POST
+ 权限: JWT Token认证 + 管理员权限
+ 参数: zhanghao (管理员账号)
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 获取前端传递的账号
+ zhanghao = request.data.get('zhanghao')
+ if not zhanghao:
+ return Response({
+ 'code': 1,
+ 'msg': '账号不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取当前用户(UserMain实例)
+ user_main = request.user
+
+ # 验证账号:比较user_main的phone与前端传递的zhanghao
+ if user_main.phone != zhanghao:
+ return Response({
+ 'code': 1,
+ 'msg': '账号不匹配',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 验证用户类型是否为管理员
+ if user_main.user_type != 'admin':
+ return Response({
+ 'code': 1,
+ 'msg': '无权限',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 验证管理员扩展表是否存在
+ try:
+ admin_profile = user_main.admin_profile
+ except AdminProfile.DoesNotExist:
+ return Response({
+ 'code': 1,
+ 'msg': '无权限',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 查询图片配置
+ rule_image = Tupianpeizhi.objects.filter(leixing=1).first()
+ default_avatar = Tupianpeizhi.objects.filter(leixing=2).first()
+
+ # 查询群配置
+ qun_configs = Qunpeizhi.objects.all()
+ qq_groups = []
+ for config in qun_configs:
+ qq_groups.append({
+ 'peizhiid': config.peizhiid,
+ 'neirong': config.neirong if config.neirong else '',
+ 'qunid': config.qunid if config.qunid else '',
+ 'jieshao': config.jieshao if config.jieshao else ''
+ })
+
+ # 查询利率配置
+ platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first()
+ shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='3').first()
+ # 🔴【新增】打手提现抽成利率(字符5)
+ dashou_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='5').first()
+ # 🔴【新增】管事提现抽成利率(字符6)
+ guanshi_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='6').first()
+
+ # 构建返回数据
+ data = {
+ 'images': {
+ 'rule_image': rule_image.tupian if rule_image and rule_image.tupian else '',
+ 'default_avatar': default_avatar.tupian if default_avatar and default_avatar.tupian else ''
+ },
+ 'qq_groups': qq_groups,
+ 'rates': {
+ 'platform_rate': float(platform_rate_obj.lilu) if platform_rate_obj and platform_rate_obj.lilu else 0.0,
+ 'shop_rate': float(shop_rate_obj.lilu) if shop_rate_obj and shop_rate_obj.lilu else 0.0,
+ # 🔴【新增】打手提现抽成利率
+ 'dashou_withdraw_rate': float(
+ dashou_withdraw_obj.lilu) if dashou_withdraw_obj and dashou_withdraw_obj.lilu else 0.0,
+ # 🔴【新增】管事提现抽成利率
+ 'guanshi_withdraw_rate': float(
+ guanshi_withdraw_obj.lilu) if guanshi_withdraw_obj and guanshi_withdraw_obj.lilu else 0.0
+
+ }
+ }
+
+ return Response({
+ 'code': 0,
+ 'msg': 'success',
+ 'data': data
+ })
+
+ except Exception as e:
+ return Response({
+ 'code': 99,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class AdminUploadImageView(APIView):
+ """
+ 管理员上传配置图片接口
+ URL: /peizhi/adxgtp
+ 方法: POST
+ 权限: JWT Token认证 + 管理员权限
+ 参数: zhanghao, leixing (1:打手规则图片, 2:默认头像图片), image (文件)
+ """
+ permission_classes = [permissions.IsAuthenticated]
+ parser_classes = (MultiPartParser, FormParser)
+
+ def post(self, request):
+ try:
+ zhanghao = request.data.get('zhanghao')
+ leixing = request.data.get('leixing')
+ image_file = request.FILES.get('image')
+
+ if not zhanghao:
+ return Response({
+ 'code': 1,
+ 'msg': '账号不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not leixing:
+ return Response({
+ 'code': 2,
+ 'msg': '图片类型不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not image_file:
+ return Response({
+ 'code': 3,
+ 'msg': '请选择图片文件',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证用户权限
+ user_main = request.user
+ if user_main.phone != zhanghao:
+ return Response({
+ 'code': 1,
+ 'msg': '账号不匹配',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ if user_main.user_type != 'admin':
+ return Response({
+ 'code': 1,
+ 'msg': '无权限',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ admin_profile = user_main.admin_profile
+ except AdminProfile.DoesNotExist:
+ return Response({
+ 'code': 1,
+ 'msg': '无权限',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 验证图片类型
+ if leixing not in ['1', '2']:
+ return Response({
+ 'code': 4,
+ 'msg': '图片类型错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证图片文件
+ is_valid, error_msg = validate_image(image_file)
+ if not is_valid:
+ return Response({
+ 'code': 5,
+ 'msg': f'图片验证失败: {error_msg}',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 根据类型确定固定相对URL和文件名
+ if leixing == '1':
+ # 打手规则图片
+ fixed_relative_url = 'a_long/dashouguize.jpg'
+ oss_file_name = 'dashouguize.jpg'
+ else:
+ # 默认头像图片
+ fixed_relative_url = 'a_long/morentouxiang.jpg'
+ oss_file_name = 'morentouxiang.jpg'
+
+ # 构建OSS文件路径
+ oss_file_path = fixed_relative_url # a_long/dashouguize.jpg 或 a_long/morentouxiang.jpg
+
+ # 上传到OSS(覆盖原有文件)
+ image_file.seek(0) # 重置文件指针
+ full_url = upload_to_oss(image_file, oss_file_path)
+
+ if not full_url:
+ return Response({
+ 'code': 6,
+ 'msg': '图片上传到存储桶失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 更新数据库
+ with transaction.atomic():
+ # 获取或创建图片配置记录
+ tupian_peizhi, created = Tupianpeizhi.objects.get_or_create(leixing=int(leixing))
+ tupian_peizhi.tupian = fixed_relative_url
+ tupian_peizhi.save()
+
+ return Response({
+ 'code': 0,
+ 'msg': '图片上传成功',
+ 'data': {
+ 'image_url': fixed_relative_url
+ }
+ })
+
+ except Exception as e:
+ return Response({
+ 'code': 99,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class AdminUpdateConfigView(APIView):
+ """
+ 管理员更新系统配置接口(QQ群、利率)- 已修复
+ URL: /peizhi/adupdate
+ 方法: POST
+ 权限: JWT Token认证 + 管理员权限
+ 参数:
+ - zhanghao: 管理员账号
+ - qq_groups: [{"peizhiid": 1, "neirong": "", "qunid": "", "jieshao": ""}, ...] (可选)
+ - rates: {"platform_rate": 0.1, "shop_rate": 0.2} (可选)
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ try:
+ zhanghao = request.data.get('zhanghao')
+ if not zhanghao:
+ return Response({
+ 'code': 1,
+ 'msg': '账号不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证用户权限
+ user_main = request.user
+ if user_main.phone != zhanghao:
+ return Response({
+ 'code': 1,
+ 'msg': '账号不匹配',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ if user_main.user_type != 'admin':
+ return Response({
+ 'code': 1,
+ 'msg': '无权限',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ admin_profile = user_main.admin_profile
+ except AdminProfile.DoesNotExist:
+ return Response({
+ 'code': 1,
+ 'msg': '无权限',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ response_data = {}
+
+ # 🔴 修复:更新QQ群配置 - 确保根据peizhiid正确更新对应的记录
+ qq_groups = request.data.get('qq_groups')
+ if qq_groups and isinstance(qq_groups, list):
+ updated_groups = []
+ for group_data in qq_groups:
+ peizhiid = group_data.get('peizhiid')
+ if not peizhiid:
+ continue
+
+ # 🔴 修复:使用filter()确保查询到正确的记录
+ qun_configs = Qunpeizhi.objects.filter(peizhiid=peizhiid)
+
+ if qun_configs.exists():
+ # 如果存在多个相同peizhiid的记录,取第一个(应该只有一个)
+ qun_config = qun_configs.first()
+
+ # 更新字段
+ neirong = group_data.get('neirong')
+ qunid = group_data.get('qunid')
+ jieshao = group_data.get('jieshao')
+
+ if neirong is not None:
+ qun_config.neirong = neirong or ''
+ if qunid is not None:
+ qun_config.qunid = qunid or ''
+ if jieshao is not None:
+ qun_config.jieshao = jieshao or ''
+
+ qun_config.save()
+ else:
+ # 如果不存在,创建新的(使用提供的peizhiid)
+ qun_config = Qunpeizhi.objects.create(
+ peizhiid=peizhiid,
+ neirong=group_data.get('neirong', '') or '',
+ qunid=group_data.get('qunid', '') or '',
+ jieshao=group_data.get('jieshao', '') or ''
+ )
+
+ updated_groups.append({
+ 'peizhiid': qun_config.peizhiid,
+ 'neirong': qun_config.neirong,
+ 'qunid': qun_config.qunid,
+ 'jieshao': qun_config.jieshao
+ })
+
+ if updated_groups:
+ response_data['qq_groups'] = updated_groups
+
+ # 🔴 修复:更新利率配置 - 确保更新现有记录而不是创建新记录
+ rates = request.data.get('rates')
+ if rates and isinstance(rates, dict):
+ updated_rates = {}
+
+ # 平台发单利率(类型1,fadanpingtai='1')
+ platform_rate = rates.get('platform_rate')
+ if platform_rate is not None:
+ try:
+ platform_rate_float = float(platform_rate)
+ if 0 <= platform_rate_float <= 1:
+ # 🔴 修复:查询现有记录,如果不存在则创建
+ platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first()
+
+ if platform_rate_obj:
+ # 更新现有记录
+ platform_rate_obj.lilu = platform_rate_float
+ platform_rate_obj.save()
+ else:
+ # 创建新记录
+ platform_rate_obj = Lilubiao.objects.create(
+ lilu=platform_rate_float,
+ fadanpingtai='1'
+ )
+
+ updated_rates['platform_rate'] = platform_rate_float
+ else:
+ return Response({
+ 'code': 3,
+ 'msg': '平台发单利率必须在0-1之间',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+ except ValueError:
+ return Response({
+ 'code': 4,
+ 'msg': '平台发单利率格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 商家发单利率(类型2,fadanpingtai='2')
+ shop_rate = rates.get('shop_rate')
+ if shop_rate is not None:
+ try:
+ shop_rate_float = float(shop_rate)
+ if 0 <= shop_rate_float <= 1:
+ # 🔴 修复:查询现有记录,如果不存在则创建
+ shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='3').first()
+
+ if shop_rate_obj:
+ # 更新现有记录
+ shop_rate_obj.lilu = shop_rate_float
+ shop_rate_obj.save()
+ else:
+ # 创建新记录
+ shop_rate_obj = Lilubiao.objects.create(
+ lilu=shop_rate_float,
+ fadanpingtai='3'
+ )
+
+ updated_rates['shop_rate'] = shop_rate_float
+ else:
+ return Response({
+ 'code': 5,
+ 'msg': '商家发单利率必须在0-1之间',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+ except ValueError:
+ return Response({
+ 'code': 6,
+ 'msg': '商家发单利率格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+
+
+
+ # 🔴【新增】打手提现抽成利率(类型5,fadanpingtai='5')
+ dashou_withdraw_rate = rates.get('dashou_withdraw_rate')
+ if dashou_withdraw_rate is not None:
+ try:
+ dashou_withdraw_rate_float = float(dashou_withdraw_rate)
+ if 0 <= dashou_withdraw_rate_float <= 1:
+ # 查询现有记录,如果不存在则创建
+ dashou_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='5').first()
+
+ if dashou_withdraw_obj:
+ # 更新现有记录
+ dashou_withdraw_obj.lilu = dashou_withdraw_rate_float
+ dashou_withdraw_obj.save()
+ else:
+ # 创建新记录
+ dashou_withdraw_obj = Lilubiao.objects.create(
+ lilu=dashou_withdraw_rate_float,
+ fadanpingtai='5'
+ )
+
+ updated_rates['dashou_withdraw_rate'] = dashou_withdraw_rate_float
+ else:
+ return Response({
+ 'code': 7, # 注意:这里需要调整错误码,不要重复
+ 'msg': '打手提现抽成利率必须在0-1之间',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+ except ValueError:
+ return Response({
+ 'code': 8,
+ 'msg': '打手提现抽成利率格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 🔴【新增】管事提现抽成利率(类型6,fadanpingtai='6')
+ guanshi_withdraw_rate = rates.get('guanshi_withdraw_rate')
+ if guanshi_withdraw_rate is not None:
+ try:
+ guanshi_withdraw_rate_float = float(guanshi_withdraw_rate)
+ if 0 <= guanshi_withdraw_rate_float <= 1:
+ # 查询现有记录,如果不存在则创建
+ guanshi_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='6').first()
+
+ if guanshi_withdraw_obj:
+ # 更新现有记录
+ guanshi_withdraw_obj.lilu = guanshi_withdraw_rate_float
+ guanshi_withdraw_obj.save()
+ else:
+ # 创建新记录
+ guanshi_withdraw_obj = Lilubiao.objects.create(
+ lilu=guanshi_withdraw_rate_float,
+ fadanpingtai='6'
+ )
+
+ updated_rates['guanshi_withdraw_rate'] = guanshi_withdraw_rate_float
+ else:
+ return Response({
+ 'code': 9,
+ 'msg': '管事提现抽成利率必须在0-1之间',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+ except ValueError:
+ return Response({
+ 'code': 10,
+ 'msg': '管事提现抽成利率格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if updated_rates:
+ response_data['rates'] = updated_rates
+
+ # 如果没有任何更新
+ if not response_data:
+ return Response({
+ 'code': 7,
+ 'msg': '没有需要更新的数据',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ return Response({
+ 'code': 0,
+ 'msg': '更新成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ return Response({
+ 'code': 99,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+class DashouPaihangView(APIView):
+ """
+ 排行榜接口
+ 路径:/peizhi/dsph
+ 方法:POST
+ 认证:JWT Token认证
+
+ 请求参数:
+ - leixing: 1=今日收入,2=今月收入
+ - yonghu_leixing: 1=打手,2=管事,3=商家
+
+ 返回数据:
+ - code: 0=成功,其他=失败
+ - msg: 提示信息
+ - data: {
+ xianshi: True/False, # 是否展示真实数据
+ paihang_list: [ # 排行榜列表
+ {
+ uid: '用户ID',
+ touxiang: '头像相对URL',
+ nicheng: '用户昵称',
+ jine: 收入金额,
+ mingci: 排名
+ },
+ ...
+ ]
+ }
+ """
+
+ # =============== 可配置参数 ===============
+ # 每次查询返回的最大记录数(可修改)
+ ZUI_DAO_CHA_XUN_SHU_LIANG = 50
+
+ # 最少需要多少人才能展示(可修改)
+ ZUI_SHAO_REN_SHU_YAO_QIU = 1
+
+ # 第一名最低金额要求(单位:元)(可修改)
+ DI_YI_MING_ZUI_DI_JIN_E = Decimal('10.00')
+
+ # 管事第一名最低数量要求(可修改)
+ DI_YI_MING_ZUI_DI_SHU_LIANG = 1
+ # =============== 可配置参数结束 ===============
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 获取请求参数 - 修复参数类型问题
+ leixing_str = request.data.get('leixing', '1')
+ yonghu_leixing_str = request.data.get('yonghu_leixing', '1')
+
+ # 转换为整数
+ try:
+ leixing = int(leixing_str)
+ except (ValueError, TypeError):
+ leixing = 1
+
+ try:
+ yonghu_leixing = int(yonghu_leixing_str)
+ except (ValueError, TypeError):
+ yonghu_leixing = 1
+
+ # 参数验证
+ if leixing not in [1, 2]:
+ return Response({
+ 'code': 400,
+ 'msg': '参数错误:leixing必须为1(今日)或2(今月)',
+ 'data': {'xianshi': False}
+ })
+
+ if yonghu_leixing not in [1, 2, 3]:
+ return Response({
+ 'code': 400,
+ 'msg': '参数错误:yonghu_leixing必须为1(打手)、2(管事)或3(商家)',
+ 'data': {'xianshi': False}
+ })
+
+ # 根据用户类型调用不同的查询方法
+ if yonghu_leixing == 1:
+ # 打手排行榜
+ paihang_list, xianshi_flag = self.chaxun_dashou_paihang(leixing)
+ elif yonghu_leixing == 2:
+ # 管事排行榜
+ paihang_list, xianshi_flag = self.chaxun_guanshi_paihang(leixing)
+ else:
+ # 商家排行榜
+ paihang_list, xianshi_flag = self.chaxun_shangjia_paihang(leixing)
+
+ # 返回结果
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': {
+ 'xianshi': xianshi_flag,
+ 'paihang_list': paihang_list
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"排行榜查询失败: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 500,
+ 'msg': '服务器内部错误',
+ 'data': {'xianshi': False}
+ })
+
+ def chaxun_dashou_paihang(self, leixing):
+ """
+ 查询打手排行榜
+ :param leixing: 1=今日收入,2=今月收入
+ :return: (排行榜列表, 是否展示标志)
+ """
+ # 确定查询的字段
+ if leixing == 1:
+ # 今日收入
+ shouyi_ziduan = 'jinrishouyi'
+ else:
+ # 今月收入
+ shouyi_ziduan = 'jinyueshouyi'
+
+ # 查询条件:收入大于0,打手账号状态正常(1=正常)
+ # 注意:打手表中的zhanghaozhuangtai字段
+ chaxun_tiaojian = Q(**{f'{shouyi_ziduan}__gt': 0}) & Q(zhanghaozhuangtai=1)
+
+ # 查询打手扩展表,并关联用户主表
+ dashou_list = UserDashou.objects.select_related('user').filter(
+ chaxun_tiaojian
+ ).order_by(f'-{shouyi_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
+
+ # 检查是否满足展示条件
+ if len(dashou_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
+ # 人数不足,不展示
+ return [], False
+
+ # 检查第一名金额是否达到最低要求
+ if leixing == 1:
+ di_yi_ming_jine = dashou_list[0].jinrishouyi
+ else:
+ di_yi_ming_jine = dashou_list[0].jinyueshouyi
+
+ if di_yi_ming_jine < self.DI_YI_MING_ZUI_DI_JIN_E:
+ # 第一名金额不足,不展示
+ return [], False
+
+ # 构建返回数据
+ paihang_list = []
+ for index, dashou in enumerate(dashou_list):
+ # 获取用户主表信息
+ user_main = dashou.user
+
+ # 打手的昵称直接使用打手扩展表的nicheng字段
+ dashou_nicheng = dashou.nicheng or '未设置昵称'
+
+ # 获取头像(相对URL)
+ touxiang_url = user_main.avatar or ''
+
+ # 获取收入金额
+ if leixing == 1:
+ shouyi_jine = dashou.jinrishouyi
+ else:
+ shouyi_jine = dashou.jinyueshouyi
+
+ paihang_list.append({
+ 'uid': user_main.yonghuid,
+ 'touxiang': touxiang_url,
+ 'nicheng': dashou_nicheng,
+ 'jine': float(shouyi_jine), # 转换为float方便前端处理
+ 'mingci': index + 1
+ })
+
+ return paihang_list, True
+
+ def chaxun_guanshi_paihang(self, leixing):
+ """
+ 查询管事排行榜
+ :param leixing: 1=今日,2=今月
+ :return: (排行榜列表, 是否展示标志)
+ """
+ # 确定查询的字段
+ if leixing == 1:
+ # 今日充值打手数量
+ chaxun_ziduan = 'jinrichongzhi'
+ else:
+ # 今月充值打手数量
+ chaxun_ziduan = 'jinyuechongzhi'
+
+ # 查询条件:数量大于0,管事状态正常(状态1为正常)
+ # 注意:管手表中的zhuangtai字段
+ chaxun_tiaojian = Q(**{f'{chaxun_ziduan}__gt': 0}) & Q(zhuangtai=1)
+
+ # 查询管事扩展表,并关联用户主表
+ guanshi_list = UserGuanshi.objects.select_related('user').filter(
+ chaxun_tiaojian
+ ).order_by(f'-{chaxun_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
+
+ # 检查是否满足展示条件
+ if len(guanshi_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
+ # 人数不足,不展示
+ return [], False
+
+ # 检查第一名数量是否达到最低要求
+ if leixing == 1:
+ di_yi_ming_shuliang = guanshi_list[0].jinrichongzhi
+ else:
+ di_yi_ming_shuliang = guanshi_list[0].jinyuechongzhi
+
+ if di_yi_ming_shuliang < self.DI_YI_MING_ZUI_DI_SHU_LIANG:
+ # 第一名数量不足,不展示
+ return [], False
+
+ # 构建返回数据
+ paihang_list = []
+ for index, guanshi in enumerate(guanshi_list):
+ # 获取用户主表信息
+ user_main = guanshi.user
+
+ # 管事的昵称需要查询老板扩展表
+ boss_profile = UserBoss.objects.filter(user=user_main).first()
+ guanshi_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
+
+ # 获取头像(相对URL)
+ touxiang_url = user_main.avatar or ''
+
+ # 获取数量
+ if leixing == 1:
+ chongzhi_shuliang = guanshi.jinrichongzhi
+ else:
+ chongzhi_shuliang = guanshi.jinyuechongzhi
+
+ paihang_list.append({
+ 'uid': user_main.yonghuid,
+ 'touxiang': touxiang_url,
+ 'nicheng': guanshi_nicheng,
+ 'jine': float(chongzhi_shuliang), # 这里jine字段实际存储的是数量
+ 'mingci': index + 1
+ })
+
+ return paihang_list, True
+
+ def chaxun_shangjia_paihang(self, leixing):
+ """
+ 查询商家排行榜
+ :param leixing: 1=今日流水,2=今月流水
+ :return: (排行榜列表, 是否展示标志)
+ """
+ # 确定查询的字段
+ if leixing == 1:
+ # 今日流水
+ liushui_ziduan = 'jinriliushui'
+ else:
+ # 今月流水
+ liushui_ziduan = 'jinyueliushui'
+
+ # 查询条件:流水大于0,商家状态正常(状态1为正常)
+ # 注意:商家表中的zhuangtai字段
+ chaxun_tiaojian = Q(**{f'{liushui_ziduan}__gt': 0}) & Q(zhuangtai=1)
+
+ # 查询商家扩展表,并关联用户主表
+ shangjia_list = UserShangjia.objects.select_related('user').filter(
+ chaxun_tiaojian
+ ).order_by(f'-{liushui_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
+
+ # 检查是否满足展示条件
+ if len(shangjia_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
+ # 人数不足,不展示
+ return [], False
+
+ # 检查第一名金额是否达到最低要求
+ if leixing == 1:
+ di_yi_ming_liushui = shangjia_list[0].jinriliushui
+ else:
+ di_yi_ming_liushui = shangjia_list[0].jinyueliushui
+
+ if di_yi_ming_liushui < self.DI_YI_MING_ZUI_DI_JIN_E:
+ # 第一名流水不足,不展示
+ return [], False
+
+ # 构建返回数据
+ paihang_list = []
+ for index, shangjia in enumerate(shangjia_list):
+ # 获取用户主表信息
+ user_main = shangjia.user
+
+ # 商家的昵称需要查询老板扩展表
+ boss_profile = UserBoss.objects.filter(user=user_main).first()
+ shangjia_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
+
+ # 获取头像(相对URL)
+ touxiang_url = user_main.avatar or ''
+
+ # 获取流水金额
+ if leixing == 1:
+ liushui_jine = shangjia.jinriliushui
+ else:
+ liushui_jine = shangjia.jinyueliushui
+
+ paihang_list.append({
+ 'uid': user_main.yonghuid,
+ 'touxiang': touxiang_url,
+ 'nicheng': shangjia_nicheng,
+ 'jine': float(liushui_jine),
+ 'mingci': index + 1
+ })
+
+ return paihang_list, True
+
+
+
+
+
+
+
+
+
+
+class GetAdminZhanghaoView(APIView):
+ """
+ 获取管理员账号列表接口
+ 路径:/peizhi/adzhhq
+ 方法:POST
+ 权限:JWT Token + 管理员验证
+ 参数:
+ - zhanghao: 当前登录的管理员账号(字符串)
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 🔥 1. 身份验证(100%按表模型)
+ user = request.user
+
+ # 获取当前用户的手机号(CharField,字符串类型)
+ current_phone = getattr(user, 'phone', '')
+ if not current_phone:
+ return Response({
+ 'code': 401,
+ 'message': '用户手机号信息不完整',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 检查前端传递的账号是否匹配(都是字符串)
+ frontend_zhanghao = request.data.get('zhanghao', '').strip()
+ if frontend_zhanghao != current_phone:
+ return Response({
+ 'code': 403,
+ 'message': '账号不匹配,无权限访问',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 🔥 2. 验证是否为管理员(user_type是CharField,值是'admin'字符串)
+ user_type = getattr(user, 'user_type', '')
+ if user_type != 'admin':
+ # 检查是否有管理员扩展表
+ if not hasattr(user, 'admin_profile'):
+ return Response({
+ 'code': 403,
+ 'message': '无管理员权限',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 🔥 3. 查询管理员账号列表(全部按字符串查询)
+ with connection.cursor() as cursor:
+ # 🔥 查询所有管理员账号(user_type='admin' 字符串)
+ # 🔥 phone和yonghuid都是CharField,xiugaiid也是CharField
+ sql = """
+ SELECT
+ um.id,
+ um.phone,
+ um.yonghuid,
+ um.user_type,
+ COALESCE(COUNT(xjl.id), 0) as jilushuliang
+ FROM user_main um
+ LEFT JOIN xiugaijilu xjl ON um.phone = xjl.xiugaiid
+ WHERE um.user_type = 'admin'
+ GROUP BY um.id, um.phone, um.yonghuid, um.user_type
+ ORDER BY um.create_time DESC
+ """
+
+ cursor.execute(sql)
+ results = cursor.fetchall()
+
+ # 🔥 4. 处理查询结果(全部保持字符串类型)
+ zhanghao_list = []
+ for row in results:
+ zhanghao_list.append({
+ #'id': row[0],
+ 'id': str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
+ 'zhanghao':str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
+ 'yonghuid': str(row[2]) if row[2] else '', # 🔥 保持字符串
+ 'user_type': str(row[3]) if row[3] else '', # 🔥 保持字符串
+ 'jilushuliang': int(row[4]) # 只有这个是数字
+ })
+
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': zhanghao_list
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ print(f"获取管理员账号列表失败: {str(e)}")
+ traceback.print_exc()
+ return Response({
+ 'code': 500,
+ 'message': '系统错误',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class GetXiugaiJiluView(APIView):
+ """
+ 获取修改记录列表接口
+ 路径:/peizhi/adhqxg
+ 方法:POST
+ 权限:JWT Token + 管理员验证
+ 参数:
+ - zhanghao: 当前登录的管理员账号(字符串)
+ - xiugaizhe_zhanghaoid: 选中的管理员账号ID(字符串,对应phone或yonghuid)
+ - page: 页码(整数)
+ - pagesize: 每页条数(整数)
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 🔥 1. 身份验证(100%按表模型)
+ user = request.user
+
+ # 获取当前用户的手机号(CharField,字符串)
+ current_phone = getattr(user, 'phone', '')
+ if not current_phone:
+ return Response({
+ 'code': 401,
+ 'message': '用户手机号信息不完整',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 检查前端传递的账号是否匹配(字符串比较)
+ frontend_zhanghao = request.data.get('zhanghao', '')
+ if not isinstance(frontend_zhanghao, str):
+ frontend_zhanghao = str(frontend_zhanghao)
+ frontend_zhanghao = frontend_zhanghao.strip()
+
+ if frontend_zhanghao != current_phone:
+ return Response({
+ 'code': 403,
+ 'message': '账号不匹配,无权限访问',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 🔥 2. 验证是否为管理员(user_type='admin'字符串)
+ user_type = getattr(user, 'user_type', '')
+ if user_type != 'admin':
+ if not hasattr(user, 'admin_profile'):
+ return Response({
+ 'code': 403,
+ 'message': '无管理员权限',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 🔥 3. 获取参数(100%按实际类型)
+ # 🔥 xiugaizhe_zhanghaoid 是字符串(对应xiugaiid字段,CharField)
+ xiugaizhe_zhanghaoid_param = request.data.get('xiugaizhe_zhanghaoid', '')
+
+
+ # 🔥 强制转为字符串(因为数据库是CharField)
+ if xiugaizhe_zhanghaoid_param is None:
+ xiugaizhe_zhanghaoid = ''
+ else:
+ # 无论如何都转为字符串
+ xiugaizhe_zhanghaoid = str(xiugaizhe_zhanghaoid_param).strip()
+
+
+ # 🔥 分页参数转为整数
+ try:
+ page = int(request.data.get('page', 1))
+ except:
+ page = 1
+
+ try:
+ pagesize = int(request.data.get('pagesize', 10))
+ except:
+ pagesize = 10
+
+ # 🔥 4. 参数验证
+ if not xiugaizhe_zhanghaoid:
+ return Response({
+ 'code': 400,
+ 'message': '修改者账号ID不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if page < 1:
+ page = 1
+ if pagesize < 1 or pagesize > 50:
+ pagesize = 10
+
+ # 🔥 5. 高效查询(100%按表字段类型)
+ with connection.cursor() as cursor:
+ # 🔥 5.1 查询总记录数(xiugaiid是CharField,用字符串查询)
+ count_sql = """
+ SELECT COUNT(*)
+ FROM xiugaijilu
+ WHERE xiugaiid = %s
+ """
+ cursor.execute(count_sql, [xiugaizhe_zhanghaoid])
+ total_count = cursor.fetchone()[0]
+
+ # 🔥 5.2 计算分页
+ offset = (page - 1) * pagesize
+
+ # 🔥 5.3 查询当前页数据(使用参数化查询,防止SQL注入)
+ data_sql = """
+ SELECT
+ xjl.id,
+ xjl.yonghuid, -- CharField
+ xjl.xiugaiid, -- CharField
+ xjl.leixing, -- IntegerField
+ xjl.xiugaitijiao, -- DecimalField
+ xjl.xiugaitijiaoq, -- DecimalField
+ xjl.jifen, -- IntegerField
+ xjl.yjifen, -- IntegerField
+ xjl.yajin, -- DecimalField
+ xjl.yyajin, -- DecimalField
+ xjl.shangjiayue, -- DecimalField
+ xjl.shangjiayueq, -- DecimalField
+ xjl.guanshiyue, -- DecimalField
+ xjl.guanshiyueq, -- DecimalField
+ xjl.huiyuants, -- IntegerField
+ xjl.huiyuan_id, -- CharField
+ xjl.qitashuoming, -- TextField
+ xjl.create_time, -- DateTimeField
+ um.avatar -- CharField
+ FROM xiugaijilu xjl
+ LEFT JOIN user_main um ON xjl.yonghuid = um.yonghuid
+ WHERE xjl.xiugaiid = %s
+ ORDER BY xjl.create_time DESC
+ LIMIT %s OFFSET %s
+ """
+
+ cursor.execute(data_sql, [xiugaizhe_zhanghaoid, pagesize, offset])
+ records = cursor.fetchall()
+
+ # 🔥 6. 处理查询结果(严格按照字段类型处理)
+ jilu_list = []
+ for record in records:
+ # 获取修改者账号信息(都是字符串)
+ xiugaizhe_zhanghao = ''
+ with connection.cursor() as cursor2:
+ # 🔥 phone和yonghuid都是CharField,用字符串查询
+ cursor2.execute(
+ "SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1",
+ [xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid]
+ )
+ xiugaizhe_info = cursor2.fetchone()
+ if xiugaizhe_info:
+ # 🔥 都是字符串
+ xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \
+ xiugaizhe_info[1] else xiugaizhe_zhanghaoid
+
+ # 获取会员名称(如果有)
+ huiyuan_mingzi = ''
+ if record[15]: # huiyuan_id是CharField
+ with connection.cursor() as cursor3:
+ cursor3.execute(
+ "SELECT jieshao FROM huiyuan WHERE huiyuan_id = %s LIMIT 1",
+ [str(record[15])] # 🔥 转为字符串查询
+ )
+ huiyuan_info = cursor3.fetchone()
+ if huiyuan_info:
+ huiyuan_mingzi = str(huiyuan_info[0]) if huiyuan_info[0] else ''
+
+ # 🔥 严格按照前端字段名映射,保持正确类型
+ jilu_data = {
+ # 基础字段(字符串)
+ 'id': record[0],
+ 'beixiugai_yonghuid': str(record[1]) if record[1] else '', # yonghuid
+ 'xiugaizhe_id': str(record[2]) if record[2] else '', # xiugaiid
+ 'xiugaizhe_zhanghao': xiugaizhe_zhanghao, # 字符串
+ 'beixiugai_yonghuleixing': record[3], # leixing是整数
+
+ # 打手字段(Decimal转float,Integer转int)
+ 'xiugaihou_dashoutixianjine': float(record[4]) if record[4] is not None else 0.00,
+ 'xiugaiqian_dashoutixianjine': float(record[5]) if record[5] is not None else 0.00,
+ 'xiugaihou_dashoujifen': int(record[6]) if record[6] is not None else 0,
+ 'xiugaiqian_dashoujifen': int(record[7]) if record[7] is not None else 0,
+ 'xiugaihou_dashouyajin': float(record[8]) if record[8] is not None else 0.00,
+ 'xiugaiqian_dashouyajin': float(record[9]) if record[9] is not None else 0.00,
+
+ # 商家字段
+ 'xiugaihou_shangjiayue': float(record[10]) if record[10] is not None else 0.00,
+ 'xiugaiqian_shangjiayue': float(record[11]) if record[11] is not None else 0.00,
+
+ # 管事字段
+ 'xiugaihou_guanshiyue': float(record[12]) if record[12] is not None else 0.00,
+ 'xiugaiqian_guanshiyue': float(record[13]) if record[13] is not None else 0.00,
+
+ # 会员字段
+ 'tianjia_huiyuantianshu': int(record[14]) if record[14] is not None else 0,
+ 'huiyuan_id': str(record[15]) if record[15] else '', # 字符串
+ 'huiyuan_mingzi': huiyuan_mingzi, # 字符串
+
+ # 其他字段
+ 'qita_shuoming': str(record[16]) if record[16] else '', # 字符串
+ 'xiugai_time': record[17].strftime('%Y-%m-%d %H:%M:%S') if record[17] else None,
+ 'touxiang': str(record[18]) if record[18] else '' # avatar是字符串
+ }
+
+ jilu_list.append(jilu_data)
+
+ # 🔥 7. 分页控制
+ queried_records = len(jilu_list)
+ has_more = False
+
+ if queried_records == pagesize:
+ total_queried = (page - 1) * pagesize + queried_records
+ has_more = total_queried < total_count
+
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': {
+ 'list': jilu_list,
+ 'zongshuliang': total_count,
+ 'dangqian_page': page,
+ 'page_size': pagesize,
+ 'has_more': has_more,
+ 'shijichaxundediaoshuliang': queried_records
+ }
+ }, status=status.HTTP_200_OK)
+
+ except ValueError as e:
+ print(f"参数错误: {str(e)}")
+ return Response({
+ 'code': 400,
+ 'message': '参数错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ except Exception as e:
+ print(f"获取修改记录失败: {str(e)}")
+ traceback.print_exc()
+ return Response({
+ 'code': 500,
+ 'message': '系统错误',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
+
+
+
+
+class ShangjiaMobanListView(APIView):
+ """
+ 商家订单模板列表接口
+ POST /peizhi/sjddmblb
+ 权限: JWT认证
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ user = request.user
+ yonghu_id = user.yonghuid
+
+ leixing_id = request.data.get('shangpinTypeId')
+ if not leixing_id:
+ return Response({'code': 400, 'msg': '商品类型ID不能为空', 'data': {}}, status=400)
+
+ get_type = int(request.data.get('getType', 2))
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 50))
+
+ keyword = request.data.get('keyword', '')
+ min_price = request.data.get('minPrice', '')
+ label_id = request.data.get('labelId', None)
+ if label_id is not None:
+ try:
+ label_id = int(label_id)
+ except (ValueError, TypeError):
+ label_id = None
+
+ query = Q(yonghu_id=yonghu_id, leixing_id=leixing_id)
+
+ if get_type == 1: # 搜索模式
+ if keyword:
+ keyword = str(keyword).strip()
+ try:
+ mid = int(keyword)
+ query &= Q(id=mid) | Q(moban_jieshao__icontains=keyword)
+ except ValueError:
+ query &= Q(moban_jieshao__icontains=keyword)
+ if min_price:
+ try:
+ query &= Q(jiage__gte=Decimal(str(min_price)))
+ except:
+ pass
+ if label_id:
+ query &= Q(chenghao_id=label_id)
+
+ mobans = ShangjiaMoban.objects.filter(query).order_by('-id')
+ formatted = []
+ for moban in mobans:
+ texiao_json = ''
+ label_name = ''
+ if moban.chenghao_id:
+ try:
+ ch = Chenghao.objects.get(id=moban.chenghao_id)
+ label_name = ch.mingcheng
+ texiao_json = ch.texiao_miaoshu or ''
+ except Chenghao.DoesNotExist:
+ pass
+ formatted.append({
+ 'mobanId': moban.id,
+ 'shangpinTypeId': moban.leixing_id,
+ 'jieshao': moban.moban_jieshao,
+ 'jiage': str(moban.jiage),
+ 'fabushuliang': moban.fabu_shuliang,
+ 'commissionEnabled': moban.yongjin_qiangdan,
+ 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None,
+ 'labelId': moban.chenghao_id,
+ 'labelName': label_name,
+ 'texiaoJson': texiao_json,
+ 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S') if moban.create_time else ''
+ })
+ return Response({'code': 200, 'msg': '搜索成功', 'data': {'list': formatted, 'total': len(formatted), 'hasMore': False}})
+
+ # 分页模式
+ queryset = ShangjiaMoban.objects.filter(query).order_by('-id')
+ paginator = Paginator(queryset, page_size)
+ if page < 1 or page > paginator.num_pages:
+ return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400)
+
+ current_page = paginator.page(page)
+ formatted = []
+ for moban in current_page.object_list:
+ texiao_json = ''
+ label_name = ''
+ if moban.chenghao_id:
+ try:
+ ch = Chenghao.objects.get(id=moban.chenghao_id)
+ label_name = ch.mingcheng
+ texiao_json = ch.texiao_miaoshu or ''
+ except Chenghao.DoesNotExist:
+ pass
+ formatted.append({
+ 'mobanId': moban.id,
+ 'shangpinTypeId': moban.leixing_id,
+ 'jieshao': moban.moban_jieshao,
+ 'jiage': str(moban.jiage),
+ 'fabushuliang': moban.fabu_shuliang,
+ 'commissionEnabled': moban.yongjin_qiangdan,
+ 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None,
+ 'labelId': moban.chenghao_id,
+ 'labelName': label_name,
+ 'texiaoJson': texiao_json,
+ 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S') if moban.create_time else ''
+ })
+
+ has_more = current_page.has_next()
+ if len(formatted) < page_size:
+ has_more = False
+
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': {
+ 'list': formatted,
+ 'total': paginator.count,
+ 'hasMore': has_more,
+ 'currentPage': page,
+ 'pageSize': page_size,
+ 'totalPages': paginator.num_pages
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"获取模板列表失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
+
+
+
+
+
+class ShangjiaTianjiaMobanView(APIView):
+ """
+ 商家添加订单模板接口
+ POST /peizhi/sjtjddmb
+ 权限: JWT认证
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 获取当前商家用户ID
+ user = request.user
+ yonghu_id = user.yonghuid
+
+ # 2. 接收并清洗参数,避免类型错误
+ leixing_id = request.data.get('shangpinTypeId')
+ jieshao = request.data.get('jieshao', '').strip() if request.data.get('jieshao') else ''
+ jiage = request.data.get('jiage', '')
+ # 佣金相关
+ commission_enabled = request.data.get('commissionEnabled', False)
+ commission_value = request.data.get('commissionValue', None)
+ # 标签ID(称号ID)
+ label_id = request.data.get('labelId', None)
+
+ # 3. 必填验证
+ if not leixing_id or not jieshao or not jiage:
+ logger.warning(f"参数缺失 - leixing_id:{leixing_id}, jieshao:{jieshao}, jiage:{jiage}")
+ return Response({
+ 'code': 400,
+ 'msg': '缺少必填字段(订单类型、介绍、价格)',
+ 'data': {}
+ }, status=400)
+
+ # 4. 价格格式验证
+ try:
+ jiage_decimal = Decimal(str(jiage))
+ if jiage_decimal <= 0:
+ return Response({'code': 400, 'msg': '价格必须大于0', 'data': {}}, status=400)
+ if jiage_decimal > 10000:
+ return Response({'code': 400, 'msg': '价格不能超过10000元', 'data': {}}, status=400)
+ except (InvalidOperation, ValueError, TypeError):
+ return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400)
+
+ # 5. 介绍长度验证
+ if len(jieshao) > 100:
+ return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400)
+
+ # 6. 佣金验证(若启用)
+ yongjin_qiangdan = False
+ yongjin_jiage = None
+ if commission_enabled:
+ yongjin_qiangdan = True
+ if not commission_value:
+ return Response({'code': 400, 'msg': '开启佣金时,佣金金额不能为空', 'data': {}}, status=400)
+ try:
+ comm_decimal = Decimal(str(commission_value))
+ if comm_decimal <= 0:
+ return Response({'code': 400, 'msg': '佣金必须大于0', 'data': {}}, status=400)
+ yongjin_jiage = comm_decimal
+ except (InvalidOperation, ValueError, TypeError):
+ return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400)
+
+ # 7. 标签ID清洗(前端可能传数字或字符串)
+ cleaned_label_id = None
+ if label_id is not None and str(label_id).strip():
+ try:
+ cleaned_label_id = int(label_id)
+ except (ValueError, TypeError):
+ return Response({'code': 400, 'msg': '标签ID格式错误', 'data': {}}, status=400)
+
+ # 8. 验证商品类型是否存在且可用
+ try:
+ shangpin_obj = ShangpinLeixing.objects.get(
+ id=leixing_id,
+ shenhezhuangtai=1 # 审核通过
+ )
+ except ShangpinLeixing.DoesNotExist:
+ return Response({'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=404)
+
+ # 9. 检查同一用户、同一类型下是否已存在相同介绍的模板
+ if ShangjiaMoban.objects.filter(
+ yonghu_id=yonghu_id,
+ leixing_id=leixing_id,
+ moban_jieshao=jieshao
+ ).exists():
+ return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400)
+
+ # 10. 执行数据库创建(事务)
+ with transaction.atomic():
+ moban = ShangjiaMoban.objects.create(
+ yonghu_id=yonghu_id,
+ leixing_id=leixing_id,
+ moban_jieshao=jieshao,
+ jiage=jiage_decimal,
+ fabu_shuliang=0,
+ yongjin_qiangdan=yongjin_qiangdan,
+ yongjin_jiage=yongjin_jiage,
+ chenghao_id=cleaned_label_id
+ )
+
+ # 11. 返回成功数据(字段与前端对齐)
+ return Response({
+ 'code': 200,
+ 'msg': '模板创建成功',
+ 'data': {
+ 'mobanId': moban.id,
+ 'shangpinTypeId': leixing_id,
+ 'jieshao': moban.moban_jieshao,
+ 'jiage': str(moban.jiage),
+ 'fabushuliang': moban.fabu_shuliang,
+ 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S'),
+ 'commissionEnabled': moban.yongjin_qiangdan,
+ 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None,
+ 'labelId': moban.chenghao_id
+ }
+ })
+
+ except Exception as e:
+ # 打印完整错误堆栈到日志
+ logger.error(f"添加模板失败: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 500,
+ 'msg': '服务器内部错误,添加模板失败,请联系管理员',
+ 'data': {}
+ }, status=500)
+
+
+
+
+class ShangjiaShanchuMobanView(APIView):
+ """
+ 删除模板
+ POST /peizhi/sjddmbsc
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ user = request.user
+ yonghu_id = user.yonghuid
+
+ moban_id = request.data.get('mobanId')
+ leixing_id = request.data.get('shangpinTypeId')
+
+ if not all([moban_id, leixing_id]):
+ return Response({'code': 400, 'msg': '参数缺失', 'data': {}}, status=400)
+
+ try:
+ moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id, leixing_id=leixing_id)
+ except ShangjiaMoban.DoesNotExist:
+ return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404)
+
+ # 强制硬删除
+ moban.delete()
+
+ return Response({'code': 200, 'msg': '删除成功', 'data': {'mobanId': moban_id, 'shangpinTypeId': leixing_id}})
+ except Exception as e:
+ logger.error(f"删除模板失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
+
+
+
+
+
+
+class ShangjiaGengxinMobanView(APIView):
+ """
+ 商家更新订单模板接口
+ POST /peizhi/sjddmbgx
+ 权限: JWT认证
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ user = request.user
+ yonghu_id = user.yonghuid
+
+ moban_id = request.data.get('mobanId')
+ leixing_id = request.data.get('shangpinTypeId')
+ new_jieshao = request.data.get('jieshao', '').strip()
+ new_jiage = request.data.get('jiage', '').strip()
+ # 新增字段
+ commission_enabled = request.data.get('commissionEnabled')
+ commission_value = request.data.get('commissionValue')
+ label_id = request.data.get('labelId')
+
+ if not all([moban_id, leixing_id]):
+ return Response({'code': 400, 'msg': '模板ID和商品类型ID不能为空', 'data': {}}, status=400)
+
+ # 查询模板
+ try:
+ moban = ShangjiaMoban.objects.get(
+ id=moban_id,
+ yonghu_id=yonghu_id,
+ leixing_id=leixing_id
+ )
+ except ShangjiaMoban.DoesNotExist:
+ return Response({'code': 404, 'msg': '模板不存在或无权修改', 'data': {}}, status=404)
+
+ update_fields = []
+
+ # 修改介绍
+ if new_jieshao:
+ if len(new_jieshao) > 100:
+ return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400)
+ if new_jieshao != moban.moban_jieshao:
+ moban.moban_jieshao = new_jieshao
+ update_fields.append('moban_jieshao')
+
+ # 修改价格
+ if new_jiage:
+ try:
+ jiage_decimal = Decimal(new_jiage)
+ if jiage_decimal <= 0 or jiage_decimal > 10000:
+ return Response({'code': 400, 'msg': '价格必须在0.1-10000元之间', 'data': {}}, status=400)
+ if jiage_decimal != moban.jiage:
+ moban.jiage = jiage_decimal
+ update_fields.append('jiage')
+ except (InvalidOperation, ValueError, TypeError):
+ return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400)
+
+ # 修改标签
+ if label_id is not None: # 允许传空字符串或不传,传0表示清空
+ new_label = int(label_id) if label_id else None
+ if new_label != moban.chenghao_id:
+ moban.chenghao_id = new_label
+ update_fields.append('chenghao_id')
+
+ # 修改佣金设置
+ if commission_enabled is not None:
+ enabled = bool(commission_enabled)
+ if enabled:
+ if not commission_value:
+ return Response({'code': 400, 'msg': '佣金价格不能为空', 'data': {}}, status=400)
+ try:
+ comm_decimal = Decimal(commission_value)
+ if comm_decimal < 0.1 or comm_decimal > 50:
+ return Response({'code': 400, 'msg': '佣金需在0.1-50元之间', 'data': {}}, status=400)
+ if moban.yongjin_qiangdan != True or moban.yongjin_jiage != comm_decimal:
+ moban.yongjin_qiangdan = True
+ moban.yongjin_jiage = comm_decimal
+ update_fields.extend(['yongjin_qiangdan', 'yongjin_jiage'])
+ except (InvalidOperation, ValueError, TypeError):
+ return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400)
+ else:
+ if moban.yongjin_qiangdan != False or moban.yongjin_jiage is not None:
+ moban.yongjin_qiangdan = False
+ moban.yongjin_jiage = None
+ update_fields.extend(['yongjin_qiangdan', 'yongjin_jiage'])
+
+ if not update_fields:
+ return Response({'code': 400, 'msg': '没有修改内容', 'data': {}}, status=400)
+
+ # 检查介绍唯一性
+ if 'moban_jieshao' in update_fields:
+ if ShangjiaMoban.objects.filter(
+ yonghu_id=yonghu_id,
+ leixing_id=leixing_id,
+ moban_jieshao=moban.moban_jieshao
+ ).exclude(id=moban_id).exists():
+ return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400)
+
+ with transaction.atomic():
+ moban.save(update_fields=update_fields)
+
+ # 返回最新数据
+ return Response({
+ 'code': 200,
+ 'msg': '模板修改成功',
+ 'data': {
+ 'mobanId': moban.id,
+ 'shangpinTypeId': moban.leixing_id,
+ 'jieshao': moban.moban_jieshao,
+ 'jiage': str(moban.jiage),
+ 'fabushuliang': moban.fabu_shuliang,
+ 'commissionEnabled': moban.yongjin_qiangdan,
+ 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage else None,
+ 'labelId': moban.chenghao_id,
+ 'updateTime': moban.update_time.strftime('%Y-%m-%d %H:%M:%S')
+ }
+ })
+
+ except Exception as e:
+ logger = logging.getLogger(__name__)
+ logger.error(f"修改商家模板失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
+
+
+
+
+
+class ShangjiaGenerateLinkView(APIView):
+ """商家生成发单链接接口(支持模板佣金/标签)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ user = request.user
+ # 获取参数
+ shangpin_type_id = request.data.get('shangpinTypeId')
+ moban_id = request.data.get('mobanId')
+ if not all([shangpin_type_id, moban_id]):
+ return Response({'code': 400, 'msg': '缺少参数', 'data': {}}, status=400)
+
+ try:
+ shangpin_type_id = int(shangpin_type_id)
+ moban_id = int(moban_id)
+ except ValueError:
+ return Response({'code': 400, 'msg': '参数格式错误', 'data': {}}, status=400)
+
+ # 商家信息
+ try:
+ shangjia = user.shop_profile
+ except UserShangjia.DoesNotExist:
+ return Response({'code': 403, 'msg': '商家信息未完善', 'data': {}}, status=403)
+ if shangjia.zhuangtai != 1:
+ return Response({'code': 403, 'msg': '商家状态异常', 'data': {}}, status=403)
+
+ # 商品类型
+ try:
+ shangpin_leixing = ShangpinLeixing.objects.get(id=shangpin_type_id, shenhezhuangtai=1)
+ except ShangpinLeixing.DoesNotExist:
+ return Response({'code': 400, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=400)
+
+ # 查询模板
+ try:
+ moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=user.yonghuid, leixing_id=shangpin_type_id)
+ except ShangjiaMoban.DoesNotExist:
+ return Response({'code': 400, 'msg': '模板不存在', 'data': {}}, status=400)
+
+ jiage = moban.jiage
+ if jiage > shangjia.yue:
+ return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元', 'data': {}}, status=400)
+
+ # 利率
+ try:
+ lilu = float(Lilubiao.objects.get(fadanpingtai='3').lilu) or 1.0
+ except Lilubiao.DoesNotExist:
+ lilu = 1.0
+ dashou_fencheng = Decimal(str(round(float(jiage) * lilu, 2)))
+
+ # 抢单要求:根据模板是否启用佣金
+ chenghao_id = moban.chenghao_id
+ if moban.yongjin_qiangdan and moban.yongjin_jiage is not None:
+ final_yaoqiuleixing = 2 # 佣金模式
+ final_yongjin = moban.yongjin_jiage
+ final_huiyuan_id = ''
+ else:
+ final_yaoqiuleixing = shangpin_leixing.yaoqiuleixing or 1
+ final_yongjin = None
+ final_huiyuan_id = shangpin_leixing.huiyuan_id or ''
+
+ # 获取标签名称
+ label_name = ''
+ if chenghao_id:
+ try:
+ label_name = Chenghao.objects.get(id=chenghao_id).mingcheng
+ except Chenghao.DoesNotExist:
+ pass
+
+ # 生成ID和Token
+ def generate_dingdan_id():
+ timestamp = int(time.time() * 1000)
+ process_id = os.getpid() % 1000
+ random_num = random.randint(1000, 9999)
+ return f"SJ{timestamp}{process_id:03d}{random_num}"
+
+ def generate_secure_token(dd_id, uid):
+ timestamp = int(time.time() * 1_000_000)
+ salt = secrets.token_hex(8)
+ raw = f"{timestamp}:{dd_id}:{uid}:{salt}"
+ token = hashlib.sha256(raw.encode()).hexdigest()[:24]
+ # 确保唯一性(极小概率重复再试一次)
+ if ShangjiaLianjie.objects.filter(lianjie_token=token).exists():
+ return generate_secure_token(dd_id, uid)
+ return token
+
+ def generate_link_url(token):
+ base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/')
+ return f"{base}/order/{token}"
+
+ with transaction.atomic():
+ dingdan_id = generate_dingdan_id()
+ secure_token = generate_secure_token(dingdan_id, user.yonghuid)
+
+ # 创建订单主表
+ dingdan = Dingdan.objects.create(
+ dingdan_id=dingdan_id,
+ zhuangtai=13,
+ fadan_pingtai=2,
+ jine=jiage,
+ dashou_fencheng=dashou_fencheng,
+ leixing_id=shangpin_type_id,
+ yaoqiuleixing=final_yaoqiuleixing,
+ huiyuan_id=final_huiyuan_id,
+ yongjin=final_yongjin,
+ jieshao=moban.moban_jieshao[:2000],
+ user1_id=f"Sj{user.yonghuid}",
+ tupian=user.avatar or '',
+ )
+
+ # 商家扩展表
+ DingdanShangjia.objects.create(
+ dingdan=dingdan,
+ shangjia_id=user.yonghuid,
+ sjnicheng=shangjia.nicheng or f"商家{user.yonghuid}",
+ )
+
+ # 关联标签
+ if chenghao_id:
+ try:
+ ch = Chenghao.objects.get(id=chenghao_id)
+ DingdanBiaoqian.objects.create(dingdan=dingdan, chenghao=ch)
+ except Chenghao.DoesNotExist:
+ logger.warning(f"称号{chenghao_id}不存在,跳过关联")
+
+ # 更新商家余额和统计
+ UserShangjia.objects.filter(id=shangjia.id).update(
+ fabu=F('fabu') + 1,
+ yue=F('yue') - jiage,
+ jinridingdan=F('jinridingdan') + 1,
+ jinriliushui=F('jinriliushui') + jiage,
+ jinyuedingdan=F('jinyuedingdan') + 1,
+ jinyueliushui=F('jinyueliushui') + jiage,
+ )
+
+ # 生成链接并保存
+ link_url = generate_link_url(secure_token)
+ lianjie_obj = ShangjiaLianjie.objects.create(
+ yonghu_id=user.yonghuid,
+ leixing_id=shangpin_type_id,
+ moban_id=moban_id,
+ lianjie=link_url,
+ lianjie_token=secure_token,
+ dingdan_id=dingdan_id,
+ is_used=False,
+ youxiao_shijian=timezone.now() + timezone.timedelta(days=1)
+ )
+
+ # 更新模板发布数量
+ ShangjiaMoban.objects.filter(id=moban_id).update(fabu_shuliang=F('fabu_shuliang') + 1)
+ shangjia.refresh_from_db()
+
+ logger.info(f"链接生成成功 - 订单ID: {dingdan_id}, 标签: {label_name}")
+
+ return Response({
+ 'code': 200,
+ 'msg': '链接生成成功',
+ 'data': {
+ 'linkUrl': link_url,
+ 'newBalance': str(shangjia.yue),
+ 'dingdanId': dingdan_id,
+ 'dingdanJieshao': moban.moban_jieshao,
+ 'jiage': str(jiage),
+ 'labelId': chenghao_id,
+ 'labelName': label_name,
+ 'youxiaoShijian': lianjie_obj.youxiao_shijian.strftime('%Y-%m-%d %H:%M:%S'),
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"生成链接事务失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '系统错误,生成链接失败', 'data': {}}, status=500)
+
+
+
+
+class ShangjiaLianjieListView(APIView):
+ """获取已生成的链接列表(支持筛选是否使用)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ user = request.user
+ yonghu_id = user.yonghuid
+
+ moban_id = request.data.get('mobanId')
+ used = int(request.data.get('used', 0)) # 0未使用,1已使用
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 5))
+ page_size = min(page_size, 50)
+
+ if not moban_id:
+ return Response({'code': 400, 'msg': '模板ID不能为空', 'data': {}}, status=400)
+
+ # 校验模板归属
+ try:
+ moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id)
+ except ShangjiaMoban.DoesNotExist:
+ return Response({'code': 404, 'msg': '模板不存在或无权查看', 'data': {}}, status=404)
+
+ links = ShangjiaLianjie.objects.filter(
+ yonghu_id=yonghu_id,
+ moban_id=moban_id,
+ is_used=(used == 1)
+ ).order_by('-create_time')
+
+ paginator = Paginator(links, page_size)
+ if page < 1 or page > paginator.num_pages:
+ return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400)
+
+ current_page = paginator.page(page)
+ data_list = []
+ for link in current_page.object_list:
+ data_list.append({
+ 'id': link.id,
+ 'lianjie': link.lianjie,
+ 'lianjie_token': link.lianjie_token,
+ 'is_used': link.is_used,
+ 'dingdan_id': link.dingdan_id,
+ 'youxiao_shijian': link.youxiao_shijian.strftime('%Y-%m-%d %H:%M:%S') if link.youxiao_shijian else '',
+ 'create_time': link.create_time.strftime('%Y-%m-%d %H:%M:%S')
+ })
+
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': {
+ 'list': data_list,
+ 'total': paginator.count,
+ 'hasMore': current_page.has_next(),
+ 'currentPage': page,
+ 'pageSize': page_size
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"获取链接列表失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
+
+
+
+
+
+
+class IPUtils:
+ """IP工具类"""
+
+ @staticmethod
+ def get_client_ip(request):
+ """
+ 获取客户端真实IP,处理代理情况
+ 优先级: X-Real-IP > X-Forwarded-For > remote_addr
+ """
+ ip = None
+
+ try:
+ # 1. 尝试从 X-Real-IP 获取
+ x_real_ip = request.META.get('HTTP_X_REAL_IP')
+ if x_real_ip:
+ ip = x_real_ip.strip()
+ logger.info(f"从 X-Real-IP 获取IP: {ip}")
+ return ip
+
+ # 2. 尝试从 X-Forwarded-For 获取(处理代理链)
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ if x_forwarded_for:
+ # 可能有多个代理IP,第一个是真实IP
+ ips = x_forwarded_for.split(',')
+ for possible_ip in ips:
+ possible_ip = possible_ip.strip()
+ if possible_ip and possible_ip.lower() != 'unknown':
+ ip = possible_ip
+ logger.info(f"从 X-Forwarded-For 获取IP: {ip}")
+ return ip
+
+ # 3. 使用 remote_addr
+ ip = request.META.get('REMOTE_ADDR', '')
+ logger.info(f"从 REMOTE_ADDR 获取IP: {ip}")
+
+ except Exception as e:
+ logger.error(f"获取IP失败: {str(e)}", exc_info=True)
+ ip = '0.0.0.0'
+
+ return ip
+
+ @staticmethod
+ def validate_ip(ip):
+ """验证IP格式是否合法"""
+ if not ip:
+ return False
+
+ # IPv4正则
+ ipv4_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
+
+ # IPv6正则(简化版)
+ ipv6_pattern = r'^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$'
+
+ return bool(re.match(ipv4_pattern, ip) or re.match(ipv6_pattern, ip))
+
+ @staticmethod
+ def is_private_ip(ip):
+ """判断是否为内网IP"""
+ if not ip:
+ return False
+
+ # 内网IP段
+ private_ranges = [
+ ('10.0.0.0', '10.255.255.255'),
+ ('172.16.0.0', '172.31.255.255'),
+ ('192.168.0.0', '192.168.255.255'),
+ ('127.0.0.0', '127.255.255.255'),
+ ]
+
+ try:
+ # 将IP转换为整数比较
+ def ip_to_int(ip):
+ parts = ip.split('.')
+ return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3])
+
+ ip_num = ip_to_int(ip)
+ for start, end in private_ranges:
+ if ip_to_int(start) <= ip_num <= ip_to_int(end):
+ return True
+ except:
+ pass
+
+ return False
+
+
+class OrderLinkThrottle(SimpleRateThrottle):
+ """订单链接访问频率限制"""
+
+ scope = 'order_link'
+ rate = '10/minute' # 每分钟最多10次
+
+ def get_cache_key(self, request, view):
+ # 基于IP和接口路径进行限制
+ ip = IPUtils.get_client_ip(request)
+ if not ip or IPUtils.is_private_ip(ip):
+ # 内网IP或不合法IP,使用默认
+ ip = 'default'
+
+ return f'throttle_{view.__class__.__name__}_{ip}'
+
+
+
+
+
+
+
+
+class KehuGetDingdanLianjieView(APIView):
+ """
+ 客户获取订单链接信息接口
+ 路径:GET /peizhi/khhqddlj?token=xxx
+ 返回字段包括:订单信息、打手标识、商家标识、俱乐部配置(客服链接、存储桶、GoEasy AppKey等)
+ """
+
+ throttle_classes = [OrderLinkThrottle]
+ permission_classes = [] # 无需认证,任何人都可访问
+
+ def get(self, request):
+ """
+ 处理GET请求,获取订单链接信息
+ """
+ # 1. 记录请求开始
+ logger.info("=== 客户获取订单链接信息接口开始 ===")
+
+ # 2. 获取客户端真实IP
+ client_ip = IPUtils.get_client_ip(request)
+ logger.info(f"客户端IP: {client_ip}")
+
+ # 3. 验证IP合法性
+ if not IPUtils.validate_ip(client_ip):
+ logger.warning(f"IP格式不合法: {client_ip}")
+ return Response({
+ 'code': 400,
+ 'msg': '网络连接异常',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 获取Token参数
+ token = request.query_params.get('token', '')
+ if not token:
+ logger.warning(f"Token参数缺失 - IP: {client_ip}")
+ return Response({
+ 'code': 400,
+ 'msg': '缺少链接参数',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ logger.info(f"接收到Token: {token[:8]}...")
+
+ # 5. 查询链接记录
+ try:
+ lianjie_obj = ShangjiaLianjie.objects.select_related().get(lianjie_token=token)
+ logger.info(f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}")
+ except ShangjiaLianjie.DoesNotExist:
+ logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}")
+ return Response({
+ 'code': 404,
+ 'msg': '链接不存在或已失效',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ logger.error(f"查询链接异常: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 500,
+ 'msg': '系统错误,请稍后重试',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 6. 更新访问者IP(无论是否已使用,都记录最新访问IP)
+ try:
+ lianjie_obj.shiyongzhe_ip = client_ip
+ lianjie_obj.save(update_fields=['shiyongzhe_ip'])
+ logger.info(f"更新访问者IP成功: {client_ip}")
+ except Exception as e:
+ logger.warning(f"更新IP失败: {str(e)}")
+ # 继续处理,不影响主要逻辑
+
+ # 7. 检查链接有效期
+ '''now = timezone.now()
+ if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now:
+ logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}, 当前: {now}")
+ return Response({
+ 'code': 400,
+ 'msg': '链接已过期',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)'''
+
+ # 8. 准备返回数据
+ response_data = {
+ 'is_used': lianjie_obj.is_used,
+ 'dingdan_id': lianjie_obj.dingdan_id or '',
+ 'youxiao_shijian': lianjie_obj.youxiao_shijian.strftime(
+ '%Y-%m-%d %H:%M:%S') if lianjie_obj.youxiao_shijian else '',
+ }
+
+ # 9. 获取订单信息
+ if lianjie_obj.dingdan_id:
+ try:
+ # 查询订单主表
+ dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id)
+
+ # 获取商家昵称
+ shangjia_nicheng = ''
+ try:
+ user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id)
+ shangjia_obj = user_main.shop_profile
+ shangjia_nicheng = shangjia_obj.nicheng
+ except (UserMain.DoesNotExist, UserShangjia.DoesNotExist):
+ logger.warning(f"商家信息不存在 - 用户ID: {lianjie_obj.yonghu_id}")
+
+ # ========== 新增:确定俱乐部配置 ==========
+ # 默认头像(相对路径)
+ default_avatar = 'a_long/morentouxiang.jpg'
+ # 初始化配置字段默认值
+ kefu_link = ''
+ oss_url = ''
+ goeasy_appkey = ''
+
+ # 判断是否为跨平台订单(保留,但不影响配置选择)
+ is_cross = dingdan_obj.is_cross if hasattr(dingdan_obj, 'is_cross') else 0
+
+ # 始终使用我方俱乐部配置
+ try:
+ club_config = ClubConfig.objects.filter(is_self=1).first()
+ if club_config:
+ logger.info(f"使用我方俱乐部配置,club_id={club_config.club_id}")
+ kefu_link = club_config.customer_service_api_url or ''
+ oss_url = club_config.storage_bucket_domain or ''
+ goeasy_appkey = club_config.chat_api_key or ''
+ else:
+ logger.warning("未找到我方俱乐部配置(is_self=1)")
+ except Exception as e:
+ logger.error(f"查询我方俱乐部配置异常: {str(e)}", exc_info=True)
+
+ # ========== 构建订单信息 ==========
+ dingdan_info = {
+ 'jieshao': dingdan_obj.jieshao or '',
+ 'jiage': str(dingdan_obj.jine) if dingdan_obj.jine is not None else '0.00', # 金额字段名是 jine
+ 'shangjia_mingcheng': shangjia_nicheng,
+ 'zhuangtai': dingdan_obj.zhuangtai,
+ 'is_cross': is_cross,
+ # 新增配置字段
+ 'kefu_link': kefu_link,
+ 'oss_url': oss_url,
+ 'default_avatar': default_avatar,
+ 'goeasy_appkey': goeasy_appkey,
+ # 前端用于连接GoEasy的自身用户ID(固定B前缀+商家ID)
+ 'self_user_id': f"B{lianjie_obj.yonghu_id}" if lianjie_obj.yonghu_id else '',
+ }
+
+ # 如果已使用,返回已填写的信息
+ if lianjie_obj.is_used:
+ dingdan_info.update({
+ 'youxi_nicheng': dingdan_obj.nicheng or '',
+ 'zhiding_dashou': dingdan_obj.zhiding_id or '',
+ 'beizhu': dingdan_obj.beizhu or '',
+ })
+
+ # 获取聊天相关标识
+ # 9.1 获取接单打手ID
+ jiedan_dashou_id = dingdan_obj.jiedan_dashou_id
+ dashou_biaoshi = f'Ds{jiedan_dashou_id}' if jiedan_dashou_id else ''
+
+ # 9.2 获取商家标识
+ shangjia_biaoshi = ''
+ try:
+ shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').filter(
+ dingdan__dingdan_id=lianjie_obj.dingdan_id
+ ).first()
+
+ if shangjia_kuozhan and shangjia_kuozhan.shangjia_id:
+ shangjia_biaoshi = f'Sj{shangjia_kuozhan.shangjia_id}'
+ logger.info(f"生成商家标识: {shangjia_biaoshi}")
+ else:
+ logger.info(f"订单 {lianjie_obj.dingdan_id} 没有商家订单扩展信息")
+ except Exception as e:
+ logger.error(f"查询商家订单扩展异常: {str(e)}", exc_info=True)
+
+ # 添加聊天标识
+ dingdan_info.update({
+ 'dashou_biaoshi': dashou_biaoshi,
+ 'shangjia_biaoshi': shangjia_biaoshi,
+ 'you_dashou': bool(dashou_biaoshi),
+ })
+
+ response_data.update(dingdan_info)
+
+ except Dingdan.DoesNotExist:
+ logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}")
+ response_data.update({
+ 'jieshao': '',
+ 'jiage': '0.00',
+ 'shangjia_mingcheng': '',
+ 'zhuangtai': None,
+ 'dashou_biaoshi': '',
+ 'shangjia_biaoshi': '',
+ 'you_dashou': False,
+ 'is_cross': 0,
+ 'kefu_link': '',
+ 'oss_url': '',
+ 'default_avatar': 'a_long/morentouxiang.jpg',
+ 'goeasy_appkey': '',
+ 'self_user_id': '',
+ })
+ except Exception as e:
+ logger.error(f"获取订单信息异常: {str(e)}", exc_info=True)
+ response_data.update({
+ 'jieshao': '',
+ 'jiage': '0.00',
+ 'shangjia_mingcheng': '',
+ 'zhuangtai': None,
+ 'dashou_biaoshi': '',
+ 'shangjia_biaoshi': '',
+ 'you_dashou': False,
+ 'is_cross': 0,
+ 'kefu_link': '',
+ 'oss_url': '',
+ 'default_avatar': 'a_long/morentouxiang.jpg',
+ 'goeasy_appkey': '',
+ 'self_user_id': '',
+ })
+ else:
+ # 如果订单ID为空
+ logger.warning(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}")
+ response_data.update({
+ 'jieshao': '',
+ 'jiage': '0.00',
+ 'shangjia_mingcheng': '',
+ 'zhuangtai': None,
+ 'dashou_biaoshi': '',
+ 'shangjia_biaoshi': '',
+ 'you_dashou': False,
+ 'is_cross': 0,
+ 'kefu_link': '',
+ 'oss_url': '',
+ 'default_avatar': 'a_long/morentouxiang.jpg',
+ 'goeasy_appkey': '',
+ 'self_user_id': '',
+ })
+
+ # 10. 判断订单状态是否允许修改
+ if not lianjie_obj.is_used and response_data.get('zhuangtai') == 13:
+ response_data['allow_modify'] = True
+ else:
+ response_data['allow_modify'] = False
+
+ # 11. 记录成功日志并返回
+ logger.info(
+ f"返回订单信息成功 - Token: {token[:8]}..., IP: {client_ip}, "
+ f"是否允许修改: {response_data['allow_modify']}, "
+ f"是否有打手: {response_data.get('you_dashou', False)}"
+ )
+
+ return Response({
+ 'code': 200,
+ 'msg': '成功',
+ 'data': response_data
+ }, status=status.HTTP_200_OK)
+
+
+
+
+
+
+
+class KehuTianxieDingdanView(APIView):
+ """
+ 客户填写订单信息接口
+ 路径:POST /peizhi/kehutx
+ """
+
+ # 添加频率限制(比查询接口更严格)
+ throttle_classes = [OrderLinkThrottle]
+ permission_classes = [] # 无需认证
+
+ def post(self, request):
+ """
+ 处理POST请求,填写订单信息
+ """
+ # 1. 记录请求开始
+ logger.info("=== 客户填写订单信息接口开始 ===")
+
+ # 2. 获取客户端真实IP
+ client_ip = IPUtils.get_client_ip(request)
+ logger.info(f"客户端IP: {client_ip}")
+
+ # 3. 验证IP合法性
+ if not IPUtils.validate_ip(client_ip):
+ logger.warning(f"IP格式不合法: {client_ip}")
+ return Response({
+ 'code': 400,
+ 'msg': '网络连接异常',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 获取请求参数
+ data = request.data
+ token = data.get('token', '')
+ youxi_nicheng = data.get('youxi_nicheng', '').strip()
+ zhiding_dashou = data.get('zhiding_dashou', '').strip() # 可选
+ beizhu = data.get('beizhu', '').strip() # 可选
+
+
+ # 4.0 临时关闭指定功能(如需开启,请删除或注释掉下面两行)
+ #if zhiding_dashou:
+ #return Response({'code': 8, 'msg': '指定功能尚未开启,请不要填写指定打手ID', 'data': None})
+ #if beizhu:
+ #return Response({'code': 8, 'msg': '备注功能尚未开启,请不要填写备注', 'data': None})
+
+
+ logger.info(
+ f"请求参数 - Token: {token[:8]}..., 昵称: {youxi_nicheng[:10]}..., 指定打手: {zhiding_dashou}, 备注长度: {len(beizhu)}")
+
+ # 5. 验证必填参数
+ if not token:
+ logger.warning(f"Token参数缺失 - IP: {client_ip}")
+ return Response({
+ 'code': 400,
+ 'msg': '缺少链接参数',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not youxi_nicheng:
+ logger.warning(f"游戏昵称为空 - IP: {client_ip}")
+ return Response({
+ 'code': 400,
+ 'msg': '游戏昵称不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 6. 验证游戏昵称格式
+ if len(youxi_nicheng) > 50:
+ logger.warning(f"游戏昵称过长 - IP: {client_ip}, 长度: {len(youxi_nicheng)}")
+ return Response({
+ 'code': 400,
+ 'msg': '游戏昵称不能超过50个字符',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 过滤危险字符
+ danger_chars = ['<', '>', "'", '"', '&', ';', '|']
+ for char in danger_chars:
+ if char in youxi_nicheng:
+ logger.warning(f"游戏昵称包含危险字符 - IP: {client_ip}, 字符: {char}")
+ return Response({
+ 'code': 400,
+ 'msg': '昵称包含非法字符',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 7. 验证指定打手ID格式(如果提供)
+ if zhiding_dashou and len(zhiding_dashou) > 32:
+ logger.warning(f"指定打手ID过长 - IP: {client_ip}, 长度: {len(zhiding_dashou)}")
+ return Response({
+ 'code': 400,
+ 'msg': '指定打手ID不能超过32个字符',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 8. 验证备注长度
+ if len(beizhu) > 500:
+ logger.warning(f"备注过长 - IP: {client_ip}, 长度: {len(beizhu)}")
+ return Response({
+ 'code': 400,
+ 'msg': '备注不能超过500个字符',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 9. 查询链接记录
+ try:
+ lianjie_obj = ShangjiaLianjie.objects.get(lianjie_token=token)
+ logger.info(
+ f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}, 是否已使用: {lianjie_obj.is_used}")
+ except ShangjiaLianjie.DoesNotExist:
+ logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}")
+ return Response({
+ 'code': 404,
+ 'msg': '链接不存在或已失效',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 10. 检查链接有效期
+ now = timezone.now()
+ if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now:
+ logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}")
+ return Response({
+ 'code': 400,
+ 'msg': '链接已过期',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 11. 检查链接是否已使用
+ if lianjie_obj.is_used:
+ logger.warning(f"链接已使用 - 链接ID: {lianjie_obj.id}, IP: {client_ip}")
+ return Response({
+ 'code': 400,
+ 'msg': '链接已被使用,无法重复提交',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 12. 验证指定打手(如果需要)
+ zhuangtai_xin = 1 # 默认普通订单状态
+ if zhiding_dashou:
+ # 验证打手是否存在且有效
+ try:
+ # 先查询用户主表
+ dashou_user = UserMain.objects.get(yonghuid=zhiding_dashou)
+ logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}")
+
+ # 通过反向关系获取打手扩展信息
+ try:
+ dashou_profile = dashou_user.dashou_profile
+
+ # 检查打手状态(zhuangtai)
+ if dashou_profile.zhuangtai != 1:
+ logger.warning(f"打手状态异常 - 打手ID: {zhiding_dashou}, 状态: {dashou_profile.zhuangtai}")
+ return Response({
+ 'code': 400,
+ 'msg': '指定打手当前无法接单',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 检查打手账号状态(zhanghaozhuangtai)
+ if dashou_profile.zhanghaozhuangtai != 1:
+ logger.warning(
+ f"打手账号状态异常 - 打手ID: {zhiding_dashou}, 账号状态: {dashou_profile.zhanghaozhuangtai}")
+ return Response({
+ 'code': 400,
+ 'msg': '指定打手账号异常',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证通过,设置为指定订单状态
+ zhuangtai_xin = 7 # 指定打手订单状态
+ logger.info(f"指定打手验证通过 - 打手ID: {zhiding_dashou}, 昵称: {dashou_profile.nicheng}")
+
+ except UserDashou.DoesNotExist:
+ logger.warning(f"打手扩展信息不存在 - 用户ID: {zhiding_dashou}")
+ return Response({
+ 'code': 400,
+ 'msg': '指定打手信息不完整',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ except UserMain.DoesNotExist:
+ logger.warning(f"指定打手用户不存在 - 用户ID: {zhiding_dashou}")
+ return Response({
+ 'code': 400,
+ 'msg': '指定打手不存在',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 13. 使用事务确保数据一致性
+ try:
+ with transaction.atomic():
+ # 更新链接记录
+ lianjie_obj.shiyongzhe_ip = client_ip
+ lianjie_obj.is_used = True
+ lianjie_obj.save(update_fields=['shiyongzhe_ip', 'is_used'])
+ logger.info(f"更新链接记录成功 - 链接ID: {lianjie_obj.id}, 设置为已使用")
+
+ # 查询订单
+ if not lianjie_obj.dingdan_id:
+ logger.error(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}")
+ raise ValueError("订单ID为空")
+
+ dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id)
+
+ # 检查订单状态是否为13(灰色状态)
+ if dingdan_obj.zhuangtai != 13:
+ logger.warning(
+ f"订单状态不允许修改 - 订单ID: {dingdan_obj.dingdan_id}, 当前状态: {dingdan_obj.zhuangtai}")
+ # 回滚事务
+ transaction.set_rollback(True)
+ return Response({
+ 'code': 400,
+ 'msg': '订单状态异常,无法修改',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 更新订单信息
+ dingdan_obj.nicheng = youxi_nicheng # 游戏昵称
+ dingdan_obj.beizhu = beizhu if beizhu else '' # 备注
+ dingdan_obj.zhuangtai = zhuangtai_xin # 新状态:1或7
+
+ # 如果是指定打手,设置zhiding_id
+ if zhiding_dashou and zhuangtai_xin == 7:
+ dingdan_obj.zhiding_id = zhiding_dashou
+
+ dingdan_obj.save()
+ logger.info(
+ f"更新订单成功 - 订单ID: {dingdan_obj.dingdan_id}, 新状态: {zhuangtai_xin}, 指定打手: {zhiding_dashou or '无'}")
+
+ # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用)
+ order_info = {
+ 'dingdan_id': dingdan_obj.dingdan_id,
+ 'game_type': self._get_game_type_name(dingdan_obj.leixing_id),
+ 'amount': str(dingdan_obj.jine),
+ 'order_desc': (dingdan_obj.jieshao or '')[:50],
+ }
+ dingdan_guangbo.delay(order_info) # 瞬间返回,不阻塞请求
+
+ # 获取商家信息用于返回
+ try:
+ user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id)
+ shangjia_obj = user_main.shop_profile
+ shangjia_mingcheng = shangjia_obj.nicheng
+ except:
+ shangjia_mingcheng = f"商家{lianjie_obj.yonghu_id}"
+ logger.warning(f"获取商家昵称失败,使用默认 - 用户ID: {lianjie_obj.yonghu_id}")
+
+ # ========== 新增:判断是否需要跨平台同步 ==========
+ # 获取商家ID(从链接记录)
+ shangjia_user_id = lianjie_obj.yonghu_id
+ # 跨平台条件:商家ID在白名单、订单为普通状态(zhuangtai=1)、要求类型为1、会员ID等于配置值
+ sync_order_to_partners(dingdan_obj.dingdan_id)
+ # ========== 新增结束 ==========
+
+ # 14. 返回成功响应
+ response_data = {
+ #'dingdan_id': dingdan_obj.dingdan_id,
+ #'create_time': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S'),
+ #'shangpin_mingcheng': dingdan_obj.jieshao[:50] if dingdan_obj.jieshao else '', # 截取前50字符
+ #'jiage': str(dingdan_obj.jine) if dingdan_obj.jine else '0.00',
+ 'shangjia_mingcheng': shangjia_mingcheng,
+ 'youxi_nicheng': youxi_nicheng,
+ 'zhuangtai': zhuangtai_xin,
+ 'zhuangtai_text': '普通订单' if zhuangtai_xin == 1 else '指定打手订单'
+ }
+
+ logger.info(f"订单提交成功 - 订单ID: {dingdan_obj.dingdan_id}, IP: {client_ip}")
+
+ # 新方法(行为驱动,更安全)
+ update_shangjia_daily(
+ yonghuid=lianjie_obj.yonghu_id,
+ amount=Decimal(str(dingdan_obj.jine)),
+ action=1 # 1 = 派发
+ )
+
+ return Response({
+ 'code': 200,
+ 'msg': '提交成功',
+ 'data': response_data
+ }, status=status.HTTP_200_OK)
+
+ except Dingdan.DoesNotExist:
+ logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}")
+ return Response({
+ 'code': 500,
+ 'msg': '订单数据异常',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ except Exception as e:
+ logger.error(f"处理订单异常: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 500,
+ 'msg': '系统错误,请稍后重试',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def _get_game_type_name(self, leixing_id):
+ try:
+ gt = ShangpinLeixing.objects.filter(id=leixing_id).first()
+ return gt.jieshao if gt else '游戏订单'
+ except Exception as e:
+ logger.error(f"获取游戏类型名称异常: {e}")
+ return '游戏订单'
+
+ def send_order_notification_async(self, dingdan, youxi_nicheng, zhiding_dashou, zhuangtai):
+ """异步发送订单通知(客户填写订单后)"""
+ try:
+ # 避免阻塞主线程,使用线程异步执行
+ def notification_task():
+ try:
+ # 获取游戏类型名称
+ try:
+ leixing_obj = ShangpinLeixing.objects.get(id=dingdan.leixing_id)
+ game_type = leixing_obj.jieshao
+ except:
+ game_type = '未知游戏'
+
+ # 构建订单信息
+ order_info = {
+ 'dingdan_id': dingdan.dingdan_id,
+ 'game_type': game_type,
+ 'amount': str(dingdan.jine),
+ 'order_desc': dingdan.jieshao[:50] if dingdan.jieshao else '新订单',
+ 'youxi_nicheng': youxi_nicheng,
+ 'order_type': '指定订单' if zhuangtai == 7 else '普通订单',
+ 'zhiding_dashou': zhiding_dashou or ''
+ }
+
+ # 调用微信广播发送器
+ sender = WeixinBroadcastSender()
+ result = sender.broadcast_order(order_info)
+
+ if result['success']:
+ logger.info(f"【客户填写订单】微信通知发送成功: {result['msg']}")
+ else:
+ logger.warning(f"【客户填写订单】微信通知发送失败: {result['msg']}")
+
+ except Exception as e:
+ logger.error(f"【客户填写订单】发送通知任务异常: {str(e)}")
+
+ # 启动异步线程
+ thread = threading.Thread(target=notification_task)
+ thread.daemon = True
+ thread.start()
+
+ except Exception as e:
+ logger.error(f"【客户填写订单】启动通知线程异常: {str(e)}")
+
+
+
+
+
+
+class GuanZhuAListView(APIView):
+ """
+ 获取关注阿龙页面图片列表
+ POST /peizhi/gzal
+ 需要JWT认证
+ 返回:
+ {
+ "code": 0,
+ "msg": "success",
+ "data": {
+ "images": ["相对URL1", "相对URL2", ...],
+ "last_update": "2025-03-16 12:34:56",
+ "visit_count": 123
+ }
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 查询类型为3的所有记录
+ qs = Tupianpeizhi.objects.filter(leixing=3)
+ if not qs.exists():
+ return Response({
+ 'code': 0,
+ 'msg': 'success',
+ 'data': {
+ 'images': [],
+ 'last_update': None,
+ 'visit_count': 0
+ }
+ }, status=status.HTTP_200_OK)
+
+ # 获取所有图片的相对URL
+ images = list(qs.values_list('tupian', flat=True))
+
+ # 获取最新更新时间(取所有记录的update_time最大值)
+ last_update = qs.aggregate(Max('update_time'))['update_time__max']
+ last_update_str = last_update.strftime('%Y-%m-%d %H:%M:%S') if last_update else None
+
+ # 原子性增加访问次数
+ # 先更新所有记录,再获取更新后的最大次数
+ qs.update(fangwen_cishu=F('fangwen_cishu') + 1)
+ visit_count = qs.aggregate(Max('fangwen_cishu'))['fangwen_cishu__max'] or 0
+
+ return Response({
+ 'code': 0,
+ 'msg': 'success',
+ 'data': {
+ 'images': images,
+ 'last_update': last_update_str,
+ 'visit_count': visit_count
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f"获取关注阿龙图片列表失败: {e}", exc_info=True)
+ return Response({
+ 'code': 1,
+ 'msg': '服务器内部错误',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
+
+#不用缓存,一天2000次
+class GuanshiQRCodeView(APIView):
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ logger.info("===== 开始生成管事二维码 =====")
+ current_user = request.user
+ logger.info(f"当前用户ID: {current_user.yonghuid}")
+
+ try:
+ try:
+ guanshi = current_user.guanshi_profile
+ logger.info(f"管事扩展表查询成功,状态: {guanshi.zhuangtai}")
+ except UserGuanshi.DoesNotExist:
+ logger.warning(f"用户 {current_user.yonghuid} 不是管事身份")
+ return Response(
+ {'code': 403, 'message': '当前用户不是管事身份', 'data': None},
+ status=status.HTTP_403_FORBIDDEN
+ )
+
+ if guanshi.zhuangtai != 1:
+ logger.warning(f"用户 {current_user.yonghuid} 管事状态异常: {guanshi.zhuangtai}")
+ return Response(
+ {'code': 403, 'message': '管事账号状态异常', 'data': None},
+ status=status.HTTP_403_FORBIDDEN
+ )
+
+ invite_code = guanshi.yaoqingma
+ if not invite_code:
+ logger.info(f"用户 {current_user.yonghuid} 无邀请码,开始生成")
+ invite_code = CreateInvitationCode(str(current_user.yonghuid))
+ if not VerifyInvitationCode(invite_code):
+ logger.error(f"邀请码唯一性校验失败,用户: {current_user.yonghuid}")
+ return Response(
+ {'code': 500, 'message': '邀请码生成失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ guanshi.yaoqingma = invite_code
+ guanshi.save(update_fields=['yaoqingma'])
+ logger.info(f"邀请码生成成功: {invite_code}")
+ else:
+ logger.info(f"使用已有邀请码: {invite_code}")
+
+ # 直接获取新的 access_token,不缓存
+ access_token = self.get_fresh_access_token()
+ if not access_token:
+ logger.error("获取 access_token 失败")
+ return Response(
+ {'code': 500, 'message': '获取微信access_token失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ logger.info(f"获取到 access_token (前10位): {access_token[:10]}...")
+
+ encoded_invite = urllib.parse.quote(invite_code, safe='')
+ page_path = f'pages/dashouduan/dashouduan?inviteCode={encoded_invite}'
+ wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
+ post_data = {
+ 'path': page_path,
+ 'width': 430,
+ 'auto_color': False,
+ 'line_color': {'r': 0, 'g': 0, 'b': 0}
+ }
+ logger.info(f"调用微信接口生成小程序码,path: {page_path}")
+
+ try:
+ wx_resp = requests.post(wx_url, json=post_data, timeout=10)
+ logger.info(f"微信接口响应状态码: {wx_resp.status_code}")
+ except Exception as e:
+ logger.error(f"请求微信接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '调用微信服务失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ if wx_resp.status_code != 200:
+ error_info = {}
+ if wx_resp.headers.get('content-type') == 'application/json':
+ try:
+ error_info = wx_resp.json()
+ except:
+ pass
+ errmsg = error_info.get('errmsg', '未知错误')
+ logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}")
+ if 'errcode' in error_info:
+ logger.error(f"微信错误码: {error_info.get('errcode')}")
+ return Response(
+ {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ content_type = wx_resp.headers.get('content-type', '')
+ if 'image' not in content_type:
+ try:
+ error_info = wx_resp.json()
+ errmsg = error_info.get('errmsg', '未知错误')
+ logger.error(f"微信接口返回非图片内容: {errmsg}")
+ return Response(
+ {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ except:
+ logger.error("微信接口返回未知内容,无法解析为JSON")
+ return Response(
+ {'code': 500, 'message': '生成二维码失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ timestamp = int(time.time())
+ filename = f"{current_user.yonghuid}_{timestamp}.png"
+ oss_path = f"erweima/guanshi/{filename}"
+ file_obj = io.BytesIO(wx_resp.content)
+ logger.info(f"开始上传图片到COS: {oss_path}")
+
+ try:
+ full_url = upload_to_oss(file_obj, oss_path)
+ logger.info(f"上传成功,完整URL: {full_url}")
+ except Exception as e:
+ logger.error(f"上传到COS异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '图片上传失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ if not full_url:
+ logger.error("upload_to_oss 返回了空 URL")
+ return Response(
+ {'code': 500, 'message': '图片上传失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ cos_domain = getattr(settings, 'COS_DOMAIN', '')
+ if cos_domain and full_url.startswith(cos_domain):
+ relative_path = full_url[len(cos_domain):].lstrip('/')
+ else:
+ relative_path = oss_path
+ logger.info(f"相对路径: {relative_path}")
+
+ old_url = guanshi.invite_qrcode_url
+ if old_url:
+ logger.info(f"删除旧二维码: {old_url}")
+ try:
+ delete_from_oss(old_url)
+ except Exception as e:
+ logger.warning(f"删除旧二维码失败(不影响主流程): {str(e)}")
+
+ with transaction.atomic():
+ guanshi.invite_qrcode_url = relative_path
+ guanshi.save(update_fields=['invite_qrcode_url'])
+ logger.info("数据库更新成功")
+
+ return Response({
+ 'code': 0,
+ 'message': 'success',
+ 'data': {'url': relative_path}
+ })
+
+ except Exception as e:
+ logger.error(f"生成二维码过程中发生未捕获异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ def get_fresh_access_token(self):
+ """
+ 直接请求微信接口获取新的 access_token,不缓存,保证最新。
+ """
+ appid = getattr(settings, 'WEIXIN_APPID', '')
+ secret = getattr(settings, 'WEIXIN_SECRET', '')
+ if not appid or not secret:
+ logger.error("微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置")
+ return None
+
+ url = 'https://api.weixin.qq.com/cgi-bin/token'
+ params = {
+ 'grant_type': 'client_credential',
+ 'appid': appid,
+ 'secret': secret
+ }
+ try:
+ logger.info("正在请求微信接口获取最新 access_token...")
+ resp = requests.get(url, params=params, timeout=5)
+ data = resp.json()
+ new_token = data.get('access_token')
+ if new_token:
+ logger.info("成功获取最新 access_token")
+ return new_token
+ else:
+ errcode = data.get('errcode', '未知')
+ errmsg = data.get('errmsg', '未知错误')
+ logger.error(f"获取 access_token 失败,errcode={errcode}, errmsg={errmsg}")
+ return None
+ except Exception as e:
+ logger.error(f"请求微信 access_token 接口异常: {str(e)}", exc_info=True)
+ return None
+
+
+
+
+
+
+
+class ZuzhangHaibaoView(APIView):
+ """
+ 组长专属海报二维码生成接口
+ 路径:/peizhi/zuzhanghb
+ 方法:POST
+ 权限:JWT认证,仅限组长身份
+ 返回:
+ {
+ "code": 0,
+ "message": "success",
+ "data": {
+ "url": "erweima/zuzhang/1000001_1234567890.png",
+ "yaoqingma": "ABC123XYZ"
+ }
+ }
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ current_user = request.user
+
+ # 1. 验证组长身份
+ try:
+ zuzhang = current_user.zuzhang_profile
+ except UserZuzhang.DoesNotExist:
+ return Response({
+ 'code': 403,
+ 'message': '当前用户不是组长身份',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 2. 验证组长账号状态
+ if zuzhang.zhuangtai != 1:
+ return Response({
+ 'code': 403,
+ 'message': '组长账号状态异常,无法生成海报',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 3. 获取或生成邀请码
+ invite_code = zuzhang.yaoqingma
+ if not invite_code:
+ invite_code = CreateInvitationCode(str(current_user.yonghuid))
+ if not VerifyInvitationCode(invite_code):
+ logger.error(f"组长邀请码生成失败,用户: {current_user.yonghuid}")
+ return Response({
+ 'code': 500,
+ 'message': '邀请码生成失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+ zuzhang.yaoqingma = invite_code
+ zuzhang.save(update_fields=['yaoqingma'])
+
+ # 4. 获取微信 access_token
+ access_token = self.get_cached_access_token()
+ if not access_token:
+ return Response({
+ 'code': 500,
+ 'message': '获取微信access_token失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # ===== 修改点:使用 A 接口(getwxacode) =====
+ # 对邀请码进行 URL 编码,防止特殊字符破坏路径
+ encoded_invite = urllib.parse.quote(invite_code, safe='')
+ # 构造带参数的完整路径(请根据实际页面调整)
+ page_path = f'pages/guanshiduan/guanshiduan?inviteCode={encoded_invite}'
+
+ wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
+ post_data = {
+ 'path': page_path,
+ 'width': 430,
+ 'auto_color': False,
+ 'line_color': {'r': 0, 'g': 0, 'b': 0}
+ }
+
+ try:
+ wx_resp = requests.post(wx_url, json=post_data, timeout=10)
+ except Exception as e:
+ logger.error(f"请求微信接口失败: {e}")
+ return Response({
+ 'code': 500,
+ 'message': '调用微信服务失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 处理微信返回结果
+ if wx_resp.status_code != 200:
+ error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {}
+ errmsg = error_info.get('errmsg', '未知错误')
+ logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}")
+ return Response({
+ 'code': 500,
+ 'message': f'生成二维码失败: {errmsg}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ content_type = wx_resp.headers.get('content-type', '')
+ if 'image' not in content_type:
+ try:
+ error_info = wx_resp.json()
+ errmsg = error_info.get('errmsg', '未知错误')
+ logger.error(f"微信接口返回非图片内容: {errmsg}")
+ return Response({
+ 'code': 500,
+ 'message': f'生成二维码失败: {errmsg}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+ except:
+ logger.error("微信接口返回未知内容")
+ return Response({
+ 'code': 500,
+ 'message': '生成二维码失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 5. 上传到腾讯云 COS
+ timestamp = int(time.time())
+ filename = f"{current_user.yonghuid}_{timestamp}.png"
+ oss_path = f"erweima/zuzhang/{filename}"
+ file_obj = io.BytesIO(wx_resp.content)
+
+ try:
+ full_url = upload_to_oss(file_obj, oss_path)
+ except Exception as e:
+ logger.error(f"上传到COS失败: {e}")
+ return Response({
+ 'code': 500,
+ 'message': '图片上传失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ if not full_url:
+ return Response({
+ 'code': 500,
+ 'message': '图片上传失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 6. 获取相对路径
+ cos_domain = getattr(settings, 'COS_DOMAIN', '')
+ if cos_domain and full_url.startswith(cos_domain):
+ relative_path = full_url[len(cos_domain):].lstrip('/')
+ else:
+ relative_path = oss_path
+
+ # 7. 删除旧二维码文件
+ old_url = zuzhang.haibao_url
+ if old_url:
+ delete_from_oss(old_url)
+
+ # 8. 更新数据库中的 haibao_url 字段
+ with transaction.atomic():
+ zuzhang.haibao_url = relative_path
+ zuzhang.save(update_fields=['haibao_url'])
+
+ # 9. 返回邀请码和二维码相对路径
+ return Response({
+ 'code': 0,
+ 'message': 'success',
+ 'data': {
+ 'url': relative_path,
+ 'yaoqingma': invite_code
+ }
+ })
+
+ def get_cached_access_token(self):
+ """从缓存获取微信 access_token"""
+ cache_key = 'wx_mini_access_token'
+ token = cache.get(cache_key)
+ if token:
+ return token
+
+ appid = getattr(settings, 'WEIXIN_APPID', '')
+ secret = getattr(settings, 'WEIXIN_SECRET', '')
+ if not appid or not secret:
+ logger.error("微信小程序配置缺失")
+ return None
+
+ url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}'
+ try:
+ resp = requests.get(url, timeout=5)
+ data = resp.json()
+ token = data.get('access_token')
+ expires_in = data.get('expires_in', 7200)
+ if token:
+ cache.set(cache_key, token, 7000)
+ return token
+ except Exception as e:
+ logger.error(f"获取微信access_token失败: {e}")
+ return None
+
+
+
+
+class PopupConfigView(APIView):
+ """
+ 获取指定页面的弹窗配置
+ 请求格式:POST { "pageKey": "dashouzhongxin" }
+ 返回格式:{ "code": 0, "data": { "serverTime": "...", "popups": [...] } }
+ """
+ permission_classes = [IsAuthenticated] # JWT 认证
+
+ def post(self, request):
+ # 1. 获取当前用户(已通过JWT认证,可直接使用)
+ user = request.user
+ if not user or not user.id:
+ return Response({
+ 'code': 401,
+ 'msg': '用户未登录或认证失败'
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 获取参数 pageKey
+ page_key = request.data.get('pageKey')
+ if not page_key:
+ return Response({
+ 'code': 400,
+ 'msg': '缺少参数 pageKey'
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 查询页面配置
+ try:
+ popup_page = PopupPage.objects.get(page_key=page_key, is_active=True)
+ except PopupPage.DoesNotExist:
+ # 该页面没有配置弹窗,返回空
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'serverTime': timezone.now().isoformat(),
+ 'popups': []
+ }
+ })
+
+ # 4. 查询该页面下所有启用的弹窗(且在有效时间范围内)
+ now = timezone.now()
+ popups = PopupConfig.objects.filter(
+ page=popup_page,
+ is_active=True,
+ start_time__lte=now,
+ end_time__gte=now
+ ).prefetch_related('images') # 预加载图片,避免N+1查询
+ # 按 sort_order 排序(model Meta 已定义)
+
+ # 5. 序列化
+ serializer = PopupConfigSerializer(popups, many=True)
+
+ # 6. 返回数据
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'serverTime': now.isoformat(),
+ 'ignore_user_mute': popup_page.ignore_user_mute, # 新增
+ 'popups': serializer.data
+ }
+ })
+
+
+
+
+class GetWithdrawModeView(APIView):
+ """
+ 获取提现模式配置
+ 前端请求 /peizhi/hqtxzsym
+ 返回格式: { "code": 0, "data": { "mode": 1 or 2 } }
+ """
+ permission_classes = [IsAuthenticated] # JWT 认证
+
+ def post(self, request, *args, **kwargs):
+ # 尝试获取 ID=1 的记录
+ config = WithdrawConfig.objects.filter(id=1).first()
+ # 如果存在则取实际值,否则默认 2(手动提现)
+ mode = config.mode if config else 2
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'mode': mode
+ }
+ })
+
+
+
+
+
+class CheckPhoneAuthView(APIView):
+ """
+ 判断当前登录用户是否需要强制手机号认证
+
+ 规则:
+ - 已认证过手机号(shoujihao_renzheng=True) → 不需要
+ - 未认证时,检查用户角色及其业务指标是否达到阈值:
+ 打手:有会员购买记录(Huiyuangoumai 中存在记录)
+ 商家:发布订单总数(fabu)>= 5
+ 管事:充值分佣总额(chongzhifenrun)>= 15
+ 组长:分佣总额(fenyong_zonge)>= 2
+ 老板:消费总额(zonge)>= 20
+ - 满足任一角色的阈值 → 需要认证
+ - 否则 → 不需要认证
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ user = request.user # UserMain 实例
+
+ # 已经认证过手机号,直接放行
+ if user.shoujihao_renzheng:
+ return Response({'need_auth': False})
+
+ # 检查各角色是否满足阈值条件
+ if self._dashou_meets_threshold(user):
+ return Response({'need_auth': True})
+ if self._shangjia_meets_threshold(user):
+ return Response({'need_auth': True})
+ if self._guanshi_meets_threshold(user):
+ return Response({'need_auth': True})
+ if self._zuzhang_meets_threshold(user):
+ return Response({'need_auth': True})
+ if self._boss_meets_threshold(user):
+ return Response({'need_auth': True})
+
+ # 都不满足,无需认证
+ return Response({'need_auth': False})
+
+ # ==================== 各角色阈值判断方法 ====================
+
+ def _dashou_meets_threshold(self, user):
+ """
+ 打手条件:存在会员购买记录(Huiyuangoumai 表中有该用户的记录)
+ """
+ try:
+ # 先确认用户是打手
+ UserDashou.objects.get(user=user)
+ except UserDashou.DoesNotExist:
+ return False
+ # 检查是否有任意会员购买记录
+ return Huiyuangoumai.objects.filter(yonghu_id=user.yonghuid).exists()
+
+ def _shangjia_meets_threshold(self, user):
+ """商家条件:发布订单总数 fabu >= 5"""
+ try:
+ shop = UserShangjia.objects.get(user=user)
+ except UserShangjia.DoesNotExist:
+ return False
+ return shop.fabu >= 5
+
+ def _guanshi_meets_threshold(self, user):
+ """管事条件:充值分佣总额 chongzhifenrun >= 15"""
+ try:
+ guanshi = UserGuanshi.objects.get(user=user)
+ except UserGuanshi.DoesNotExist:
+ return False
+ return guanshi.chongzhifenrun >= 15
+
+ def _zuzhang_meets_threshold(self, user):
+ """组长条件:分佣总额 fenyong_zonge >= 2"""
+ try:
+ zuzhang = UserZuzhang.objects.get(user=user)
+ except UserZuzhang.DoesNotExist:
+ return False
+ return zuzhang.fenyong_zonge >= 2
+
+ def _boss_meets_threshold(self, user):
+ """老板条件:消费总额 zonge >= 20"""
+ try:
+ boss = UserBoss.objects.get(user=user)
+ except UserBoss.DoesNotExist:
+ return False
return boss.zonge >= 20
\ No newline at end of file
diff --git a/dingdan/signals.py b/dingdan/signals.py
deleted file mode 100644
index b34f571..0000000
--- a/dingdan/signals.py
+++ /dev/null
@@ -1,614 +0,0 @@
-"""
-阿龙电竞 - 信号处理器(修复Redis导入问题)
-"""
-
-
-
-
-
-
-
-
-
-# dingdan/signals.py
-"""
-阿龙电竞 - 订单信号处理器(生产稳定版)
-经过验证,100%可靠
-"""
-
-import sys
-import importlib
-import logging
-from django.db.models.signals import pre_save
-from django.dispatch import receiver
-from django.conf import settings
-from django.utils import timezone
-
-logger = logging.getLogger(__name__)
-
-# 🔧 关键修复:确保redis模块正确加载
-def ensure_redis_loaded():
- """确保redis模块正确加载,避免导入冲突"""
- if 'redis' not in sys.modules:
- # 如果redis模块未加载,尝试导入
- try:
- import redis
- #logger.debug("✅ Redis模块正常加载")
- except ImportError:
- #logger.warning("⚠️ Redis模块未安装,尝试自动安装")
- import subprocess
- subprocess.call([sys.executable, "-m", "pip", "install", "redis==4.5.4", "-q"])
- import redis
-
- # 确保redis.Redis属性存在
- import redis
- if redis.Redis is None:
- #logger.warning("⚠️ Redis.Redis属性为None,强制重新加载模块")
- importlib.reload(redis)
-
- return redis
-
-# 预加载redis模块
-ensure_redis_loaded()
-
-@receiver(pre_save, sender='dingdan.Dingdan')
-def handle_order_status_8(sender, instance, **kwargs):
- """
- 订单状态变为8时,提交定时任务
- """
- # 跳过新增的订单
- if instance.pk is None:
- return
-
- try:
- # 动态导入,避免循环依赖
- from dingdan.models import Dingdan
-
- # 获取旧状态
- old = Dingdan.objects.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first()
-
- # 状态从 非8 变为 8
- if old != 8 and instance.zhuangtai == 8:
- #logger.info(f"📅 订单 {instance.dingdan_id} 状态变为8,提交定时任务")
-
- # 设置时间标记
- instance.status_8_time = timezone.now()
- instance.pending_dispatch = True
-
- # 使用配置中的超时时间
- expire_seconds = getattr(settings, 'ORDER_EXPIRE_SECONDS', 48 * 60 * 60)
-
- # 提交Celery任务
- try:
- from dingdan.tasks import process_expired_order
-
- task_result = process_expired_order.apply_async(
- args=[instance.dingdan_id],
- countdown=expire_seconds,
- queue='order_tasks',
- priority=9
- )
-
- #logger.info(f"✅ 任务提交成功,订单: {instance.dingdan_id}, 任务ID: {task_result.id}")
-
- # 保存任务信息
- instance.auto_task_id = task_result.id
- instance.auto_expire_at = timezone.now() + timezone.timedelta(seconds=expire_seconds)
-
- except Exception as e:
- logger.error(f"❌ 任务提交失败: {e}")
- instance.pending_dispatch = True
- instance.auto_task_id = f"failed_{timezone.now().timestamp()}"
-
- # 状态从 8 变为 非8
- elif old == 8 and instance.zhuangtai != 8:
- instance.status_8_time = None
- instance.pending_dispatch = False
- instance.auto_task_id = ''
- logger.info(f"📅 订单 {instance.dingdan_id} 状态离开8")
-
- except Exception as e:
- logger.error(f"信号处理失败: {e}")
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-# 🔥 关键修复:在最开始强制正确导入redis模块
-'''import sys
-
-# 1. 如果redis模块已加载但有问题,先移除
-if 'redis' in sys.modules:
- print(f"🔧 移除已加载的redis模块: {sys.modules['redis']}")
- del sys.modules['redis']
- # 同时移除相关模块
- for key in list(sys.modules.keys()):
- if key.startswith('redis.') or key == 'redis':
- del sys.modules[key]
-
-# 2. 重新导入redis模块
-try:
- import importlib
- redis = importlib.import_module('redis')
- print(f"✅ 重新导入redis成功: {redis}")
- print(f" Redis类: {redis.Redis}")
- print(f" Redis类是否为None: {redis.Redis is None}")
-
- # 验证Redis类是否可用
- if redis.Redis is None:
- print("❌ Redis类为None,尝试从源码级别修复")
- # 强制重新加载
- import importlib.util
- import os
-
- # 找到redis包路径
- import pkgutil
- redis_spec = pkgutil.find_loader('redis')
- if redis_spec:
- print(f" Redis包路径: {redis_spec.origin}")
- # 强制重新加载
- importlib.invalidate_caches()
- redis = importlib.import_module('redis')
- importlib.reload(redis)
- print(f" Redis类(重载后): {redis.Redis}")
-
-except Exception as e:
- print(f"❌ 导入redis失败: {e}")
- import traceback
- traceback.print_exc()
- # 尝试直接安装
- print("尝试直接安装redis包...")
- import subprocess
- subprocess.call([sys.executable, "-m", "pip", "install", "redis==4.5.4", "-q"])
-
-# 3. 确保kombu.transport.redis也能正确导入
-try:
- import kombu.transport.redis
- print(f"✅ kombu.transport.redis导入成功")
-except Exception as e:
- print(f"❌ kombu.transport.redis导入失败: {e}")
- # 修复:先导入redis,再导入kombu.transport.redis
- if 'redis' not in sys.modules:
- import redis
- # 重新导入
- import importlib
- if 'kombu.transport.redis' in sys.modules:
- del sys.modules['kombu.transport.redis']
- kombu_transport_redis = importlib.import_module('kombu.transport.redis')
- print(f"✅ 重新导入kombu.transport.redis成功")
-
-# 4. 继续原来的信号处理器代码
-import logging
-from django.db.models.signals import pre_save
-from django.dispatch import receiver
-from django.utils import timezone
-
-logger = logging.getLogger(__name__)
-
-@receiver(pre_save, sender='dingdan.Dingdan')
-def order_status_8_handler_fixed(sender, instance, **kwargs):
- """
- 修复Redis导入问题后的信号处理器
- """
- if instance.pk is None:
- return
-
- try:
- # 动态导入,避免启动时循环导入
- from dingdan.models import Dingdan
-
- # 获取旧状态
- old = Dingdan.objects.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first()
-
- # 状态从 非8 变为 8
- if old != 8 and instance.zhuangtai == 8:
- logger.info(f"📅 订单 {instance.dingdan_id} 状态变8")
-
- # 设置时间标记
- instance.status_8_time = timezone.now()
- instance.pending_dispatch = True
-
- # 获取超时时间
- expire_seconds = 60 # 测试用60秒
-
- # 🔥 再次验证redis模块
- try:
- import redis
- logger.info(f"🔍 信号处理器内redis模块状态: {redis.Redis}")
- if redis.Redis is None:
- logger.error("❌ 信号处理器内redis.Redis为None!")
- # 使用备用方案
- instance.auto_task_id = f"redis_none_{timezone.now().timestamp()}"
- return
- except Exception as e:
- logger.error(f"❌ 信号处理器内验证redis失败: {e}")
- instance.auto_task_id = f"redis_check_failed_{timezone.now().timestamp()}"
- return
-
- # 提交Celery任务
- try:
- from dingdan.tasks import process_expired_order
-
- logger.info(f"🔧 开始提交任务,订单: {instance.dingdan_id}")
-
- # 使用更安全的方式提交任务
- from celery import current_app
-
- # 验证current_app的backend
- if hasattr(current_app, 'backend'):
- logger.info(f"✅ current_app.backend存在: {current_app.backend}")
- else:
- logger.error("❌ current_app没有backend属性")
-
- task_result = process_expired_order.apply_async(
- args=[instance.dingdan_id],
- countdown=expire_seconds,
- queue='order_tasks',
- priority=9
- )
-
- logger.info(f"✅ Celery任务提交成功,订单: {instance.dingdan_id}, 任务ID: {task_result.id}")
-
- # 保存任务信息
- instance.auto_task_id = task_result.id
- instance.auto_expire_at = timezone.now() + timezone.timedelta(seconds=expire_seconds)
-
- except Exception as e:
- logger.error(f"❌ Celery任务提交失败: {type(e).__name__}: {e}")
-
- # 特别检查Redis相关错误
- if "'NoneType' object has no attribute 'Redis'" in str(e):
- logger.error("💥 检测到Redis类为None错误")
- logger.error("💥 尝试从当前环境重新导入redis模块")
-
- # 尝试修复
- try:
- import importlib
- import redis as redis_module
- importlib.reload(redis_module)
- logger.info(f"💥 重新加载redis模块后: {redis_module.Redis}")
- except Exception as reload_e:
- logger.error(f"💥 重新加载失败: {reload_e}")
-
- # 标记为待调度,由调度器处理
- instance.pending_dispatch = True
- instance.auto_task_id = f"failed_{timezone.now().timestamp()}"
-
- # 状态从 8 变为 非8
- elif old == 8 and instance.zhuangtai != 8:
- instance.status_8_time = None
- instance.pending_dispatch = False
- instance.auto_task_id = ''
- logger.info(f"📅 订单 {instance.dingdan_id} 状态离开8")
-
- except Exception as e:
- logger.error(f"订单状态变更处理失败,订单ID: {getattr(instance, 'dingdan_id', '未知')}, 错误: {e}")'''
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-'''from django.db.models.signals import pre_save
-from django.dispatch import receiver
-from django.conf import settings
-from django.utils import timezone
-import logging
-import threading
-import time
-
-from dingdan.models import Dingdan
-
-logger = logging.getLogger(__name__)
-
-
-# 在你的 dingdan/tasks.py 文件末尾添加以下代码
-
-from django.db.models.signals import pre_save
-from django.dispatch import receiver
-from django.utils import timezone
-import logging
-
-from dingdan.models import Dingdan
-
-logger = logging.getLogger(__name__)
-
-@receiver(pre_save, sender=Dingdan)
-def handle_order_status_change(sender, instance, **kwargs):
- """
- 订单状态变化监听器(纯净标记版)
- 严格遵循原始逻辑,仅更新数据库标记。
- """
- # 1. 跳过新建的订单
- if instance.pk is None:
- return
-
- # 2. 获取数据库中此订单的旧状态
- try:
- old_status = Dingdan.objects.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first()
- except Dingdan.DoesNotExist:
- return # 旧订单不存在,不处理
- except Exception as e:
- logger.error(f"获取订单 {instance.pk} 旧状态失败: {e},跳过信号处理")
- return # 出错时静默退出,绝不阻塞订单保存
-
- old_status = old_status or 0 # 防None
-
- # 3. 【核心逻辑】状态从 非8 变为 8
- if old_status != 8 and instance.zhuangtai == 8:
- instance.pending_dispatch = True # 标记为待处理
- instance.status_8_time = timezone.now() # 记录变8时间
- # 清空可能存在的旧任务信息(安全起见)
- instance.auto_task_id = ''
- instance.auto_expire_at = None
- logger.info(f"📝 订单 {instance.dingdan_id} 状态变8,已标记为待调度。")
-
- # 4. 【核心逻辑】状态从 8 变为 非8
- elif old_status == 8 and instance.zhuangtai != 8:
- # 清除所有调度标记和任务信息
- instance.pending_dispatch = False
- instance.status_8_time = None
- instance.auto_task_id = ''
- instance.auto_expire_at = None
- logger.info(f"📝 订单 {instance.dingdan_id} 状态离开8,已清除所有调度标记。")'''
-
-
-from django.db.models.signals import pre_save
-from django.dispatch import receiver
-from django.utils import timezone
-from datetime import timedelta
-from celery.result import AsyncResult
-import logging
-
-from dingdan.models import Dingdan
-from dingdan.tasks import process_expired_order # 确保导入你的任务函数
-
-'''logger = logging.getLogger(__name__)
-@receiver(pre_save, sender=Dingdan)
-def handle_order_status_change(sender, instance, **kwargs):
- """
- 监听订单状态变化(稳定生产版)- 修复配置问题
- """
- # 1. 跳过新建的订单(无旧状态可比)
- if instance.pk is None:
- return
-
- try:
- old = sender.objects.get(pk=instance.pk)
- except sender.DoesNotExist:
- return
- # dingdan/signals.py - 修改状态变8的逻辑部分
-
-
- # 2. 状态变成 8:提交任务
- if old.zhuangtai != 8 and instance.zhuangtai == 8:
- from django.conf import settings
- from celery import current_app
-
- # 🔥【核心修复】确保信号上下文中的Celery应用有正确配置
- # 直接从你的Django settings中获取Redis连接字符串,并设置给current_app
- if hasattr(settings, 'CELERY_BROKER_URL'):
- current_app.conf.update(broker_url=settings.CELERY_BROKER_URL)
- else:
- # 如果settings里没有,就按你的配置拼接(和celery.py里一致)
- redis_password = getattr(settings, 'REDIS_PASSWORD', 'Dujieduze5.3')
- redis_host = getattr(settings, 'REDIS_HOST', '172.19.0.3')
- redis_port = getattr(settings, 'REDIS_PORT', 6379)
- redis_db_celery = getattr(settings, 'REDIS_DB_CELERY', 0)
- broker_url = f'redis://:{redis_password}@{redis_host}:{redis_port}/{redis_db_celery}'
- current_app.conf.update(broker_url=broker_url)
-
- countdown_seconds = getattr(settings, 'ORDER_EXPIRE_SECONDS', 172800) # 默认48小时
-
- try:
- # 现在再提交任务
- job = process_expired_order.apply_async(
- args=[instance.dingdan_id],
- countdown=countdown_seconds,
- queue='order_tasks',
- priority=9
- )
- # ✅ 核心:将任务ID和计划时间记录到当前实例
- instance.auto_task_id = job.id
- instance.auto_expire_at = timezone.now() + timedelta(seconds=countdown_seconds)
- logger.info(f"✅ 订单 {instance.dingdan_id} 状态变8,已提交延时任务,ID: {job.id}")
- except Exception as e:
- logger.error(f"❌ 提交订单 {instance.dingdan_id} 的延时任务失败: {str(e)}")
- return # 处理完毕,直接返回
-
- # 3. 状态从 8 被改成别的 -> 尝试撤销任务 (这部分代码不变)
- if old.zhuangtai == 8 and instance.zhuangtai != 8:
- if old.auto_task_id:
- try:
- AsyncResult(old.auto_task_id).revoke()
- logger.info(f"订单 {instance.dingdan_id} 状态离开8,已撤销原任务: {old.auto_task_id}")
- except Exception as e:
- logger.warning(f"撤销订单 {instance.dingdan_id} 的任务失败: {str(e)}")
- instance.auto_task_id = ''
- instance.auto_expire_at = None'''
-
-
-
-'''def try_start_celery_task_async(dingdan_id, countdown):
- """
- 异步尝试启动Celery任务(在独立线程中运行)
- 如果Redis不可用,立即失败,不影响主流程
- """
- def _async_task():
- try:
- # 设置超时时间(500毫秒),避免长时间等待
- import socket
- socket.setdefaulttimeout(0.5)
-
- from dingdan.tasks import process_expired_order
- from celery.exceptions import TimeoutError
-
- # 尝试启动任务,设置极短的超时
- task = process_expired_order.apply_async(
- args=[dingdan_id],
- countdown=countdown,
- queue='order_tasks',
- priority=9,
- expires=countdown + 3600 # 任务1小时后过期,避免堆积
- )
-
- # 不等待任务结果,立即返回
- logger.info(f"订单 {dingdan_id} 异步任务已提交(任务ID: {task.id})")
-
- except Exception as e:
- # 记录警告,但不要影响主流程
- logger.warning(f"订单 {dingdan_id} 异步任务提交失败: {str(e)}")
- # 这个失败不影响订单保存,系统有补偿机制
-
- # 启动异步线程(后台运行,不阻塞主流程)
- thread = threading.Thread(target=_async_task)
- thread.daemon = True # 设置为守护线程,主线程退出时会自动结束
- thread.start()
-
-@receiver(pre_save, sender=Dingdan)
-def handle_order_status_change_lightweight(sender, instance, **kwargs):
- print(f"🚨【信号触发】订单ID: {getattr(instance, 'dingdan_id', 'N/A')}, 新状态: {instance.zhuangtai}")
- """
- 轻量级订单状态变化监听器
- 设计原则:
- 1. 绝对不阻塞订单保存
- 2. 不依赖外部服务可用性
- 3. 只记录,不执行
- """
- try:
- # 1. 快速检查:如果是新建订单,直接返回
- if instance.pk is None:
- return
-
- # 2. 快速获取旧状态(使用数据库当前值,不加载完整对象)
- try:
- old_status = Dingdan.objects.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first()
- except Exception:
- old_status = None
-
- # 3. 状态变化判断
- if old_status is None or old_status != instance.zhuangtai:
- # 状态发生变化,记录到订单扩展字段或日志(但不要阻塞)
- try:
- # 这里只是示例,实际可以根据需要记录状态变化历史
- pass
- except:
- pass # 即使记录失败也不影响主流程
-
- # 4. 特殊状态处理:状态变为8
- if old_status != 8 and instance.zhuangtai == 8:
- # 4.1 立即记录日志(不等待)
- logger.info(f"订单 {instance.dingdan_id} 状态变为8")
-
- # 4.2 异步尝试启动定时任务(不阻塞主流程)
- # 使用线程池或直接新线程,确保不阻塞
- try:
- # 启动异步线程尝试提交Celery任务
- if hasattr(settings, 'CELERY_BROKER_URL') and settings.CELERY_BROKER_URL:
- # 在独立线程中尝试,完全不阻塞
- threading.Thread(
- target=try_start_celery_task_async,
- args=(instance.dingdan_id, settings.ORDER_EXPIRE_SECONDS),
- daemon=True
- ).start()
- except Exception as e:
- # 即使线程启动失败也不影响主流程
- logger.warning(f"启动异步任务线程失败: {str(e)}")
-
- # 5. 状态从8变为其他
- elif old_status == 8 and instance.zhuangtai != 8:
- # 状态不再是8,记录日志即可
- # 定时任务执行时会检查状态,如果不是8会自动跳过
- logger.info(f"订单 {instance.dingdan_id} 状态从8变为{instance.zhuangtai}")
-
- except Exception as e:
- # 捕获所有异常,确保不影响订单保存
- # 只记录错误,不抛出异常
- logger.error(f"信号处理异常(已捕获,不影响订单): {str(e)}")'''
-
-
-'''from django.db.models.signals import pre_save
-from django.dispatch import receiver
-from django.conf import settings
-import logging
-
-from dingdan.models import Dingdan
-from dingdan.tasks import process_expired_order
-
-logger = logging.getLogger(__name__)
-
-@receiver(pre_save, sender=Dingdan)
-def handle_order_status_change(sender, instance, **kwargs):
- """
- 监听订单状态变化
- 当状态变为8时:启动48小时延时任务
- 当状态从8变为其他时:任务执行时会自动跳过,无需取消
- """
- try:
- # 如果是新建订单,没有旧记录,直接返回
- if instance.pk is None:
- return
-
- # 获取数据库中旧的订单记录
- old_instance = Dingdan.objects.get(pk=instance.pk)
-
- # ============ 状态从非8变为8 ============
- if old_instance.zhuangtai != 8 and instance.zhuangtai == 8:
- # 启动48小时后的延时任务(countdown单位:秒)
- task = process_expired_order.apply_async(
- args=[instance.dingdan_id],
- countdown=settings.ORDER_EXPIRE_SECONDS, # 172800秒 = 48小时
- queue='order_tasks',
- priority=9 # 高优先级
- )
-
- logger.info(
- f"订单 {instance.dingdan_id} 状态变为8,已启动48小时延时任务,"
- f"任务ID: {task.id}"
- )
-
- # 记录任务启动成功(可选,可以存储到订单表或日志表)
-
- # ============ 状态从8变为其他 ============
- # 我们不需要取消任务,因为任务执行时会检查状态是否为8
- # 如果不是状态8,任务会自动跳过处理
-
- except Dingdan.DoesNotExist:
- # 新订单,无需处理
- pass
- except Exception as e:
- logger.error(f"处理订单状态变化信号失败,订单ID: {instance.dingdan_id if hasattr(instance, 'dingdan_id') else '未知'}, 错误: {str(e)}")
- # 不抛出异常,避免影响订单保存的主流程
- # 信号处理失败不影响订单保存,有周期性检查作为备用方案'''
\ No newline at end of file
diff --git a/gunicorn.conf.py b/gunicorn.conf.py
index 1e0e690..e9550b9 100644
--- a/gunicorn.conf.py
+++ b/gunicorn.conf.py
@@ -1,28 +1,28 @@
# /opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.443/django/gunicorn.conf.py
-# 🔥 绑定的IP和端口(根据你的Nginx配置,后端在8001端口)
+# 绑定的IP和端口(根据你的Nginx配置,后端在8001端口)
bind = "127.0.0.1:8001"
-# 🔥 工作进程数(根据CPU核心数调整,建议:CPU核心数*2+1)
+# 工作进程数(根据CPU核心数调整,建议:CPU核心数*2+1)
# 查看CPU核心数:cat /proc/cpuinfo | grep "processor" | wc -l
workers = 4
-# 🔥 工作模式(使用gevent支持异步,更高效)
+# 工作模式(使用gevent支持异步,更高效)
#worker_class = "gevent"
worker_class = "sync" # 改成这个
-# 🔥 每个worker的最大请求数(防止内存泄漏)
+# 每个worker的最大请求数(防止内存泄漏)
max_requests = 1000
max_requests_jitter = 50 # 随机抖动,避免所有worker同时重启
-# 🔥 超时时间(秒)
+# 超时时间(秒)
timeout = 30
keepalive = 2
-# 🔥 预加载应用(减少内存占用,加快请求处理)
+# 预加载应用(减少内存占用,加快请求处理)
preload_app = True
-# 🔥 日志配置
+# 日志配置
# 访问日志:输出到标准输出,便于OnePanel查看
accesslog = "-"
# 错误日志:输出到标准错误
@@ -30,23 +30,23 @@ errorlog = "-"
# 日志级别
loglevel = "info"
-# 🔥 进程名称(便于识别)
+# 进程名称(便于识别)
proc_name = "a_long_dianjing_gunicorn"
-# 🔥 工作进程的用户/组(安全考虑,用非root用户运行)
+# 工作进程的用户/组(安全考虑,用非root用户运行)
# user = "www-data"
# group = "www-data"
-# 🔥 工作进程临时目录(避免权限问题)
+# 工作进程临时目录(避免权限问题)
worker_tmp_dir = "/dev/shm"
-# 🔥 限制请求数据大小(防止大文件攻击)
+# 限制请求数据大小(防止大文件攻击)
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
-# 🔥 优雅退出时间
+# 优雅退出时间
graceful_timeout = 30
-# 🔥 进程守护(如果用systemd管理,这里设为False)
+# 进程守护(如果用systemd管理,这里设为False)
daemon = False
\ No newline at end of file
diff --git a/dingdan/migrations/__init__.py b/orders/__init__.py
similarity index 100%
rename from dingdan/migrations/__init__.py
rename to orders/__init__.py
diff --git a/dingdan/admin.py b/orders/admin.py
similarity index 100%
rename from dingdan/admin.py
rename to orders/admin.py
diff --git a/dingdan/apps.py b/orders/apps.py
similarity index 88%
rename from dingdan/apps.py
rename to orders/apps.py
index 0a14712..789b4b7 100644
--- a/dingdan/apps.py
+++ b/orders/apps.py
@@ -7,4 +7,4 @@ class DingdanConfig(AppConfig):
def ready(self):
# 导入信号处理,确保信号被注册
- import dingdan.signals
\ No newline at end of file
+ import orders.signals
diff --git a/dingdan/cross_platform_tasks.py b/orders/cross_platform_tasks.py
similarity index 100%
rename from dingdan/cross_platform_tasks.py
rename to orders/cross_platform_tasks.py
diff --git a/dingdan/migrations/0001_initial.py b/orders/migrations/0001_initial.py
similarity index 100%
rename from dingdan/migrations/0001_initial.py
rename to orders/migrations/0001_initial.py
diff --git a/dingdan/migrations/0002_dingdan_auto_expire_at_dingdan_auto_task_id.py b/orders/migrations/0002_dingdan_auto_expire_at_dingdan_auto_task_id.py
similarity index 100%
rename from dingdan/migrations/0002_dingdan_auto_expire_at_dingdan_auto_task_id.py
rename to orders/migrations/0002_dingdan_auto_expire_at_dingdan_auto_task_id.py
diff --git a/dingdan/migrations/0003_dingdan_pending_dispatch_dingdan_status_8_time.py b/orders/migrations/0003_dingdan_pending_dispatch_dingdan_status_8_time.py
similarity index 100%
rename from dingdan/migrations/0003_dingdan_pending_dispatch_dingdan_status_8_time.py
rename to orders/migrations/0003_dingdan_pending_dispatch_dingdan_status_8_time.py
diff --git a/dingdan/migrations/0004_alter_tuikuanjilu_chuliid.py b/orders/migrations/0004_alter_tuikuanjilu_chuliid.py
similarity index 100%
rename from dingdan/migrations/0004_alter_tuikuanjilu_chuliid.py
rename to orders/migrations/0004_alter_tuikuanjilu_chuliid.py
diff --git a/dingdan/migrations/0005_alter_chufajilu_chuliid.py b/orders/migrations/0005_alter_chufajilu_chuliid.py
similarity index 100%
rename from dingdan/migrations/0005_alter_chufajilu_chuliid.py
rename to orders/migrations/0005_alter_chufajilu_chuliid.py
diff --git a/dingdan/migrations/0006_chufatupian.py b/orders/migrations/0006_chufatupian.py
similarity index 100%
rename from dingdan/migrations/0006_chufatupian.py
rename to orders/migrations/0006_chufatupian.py
diff --git a/dingdan/migrations/0007_chufajilu_ssliyou.py b/orders/migrations/0007_chufajilu_ssliyou.py
similarity index 100%
rename from dingdan/migrations/0007_chufajilu_ssliyou.py
rename to orders/migrations/0007_chufajilu_ssliyou.py
diff --git a/dingdan/migrations/0008_dingdan_is_cross_dingdan_partner_order_id.py b/orders/migrations/0008_dingdan_is_cross_dingdan_partner_order_id.py
similarity index 100%
rename from dingdan/migrations/0008_dingdan_is_cross_dingdan_partner_order_id.py
rename to orders/migrations/0008_dingdan_is_cross_dingdan_partner_order_id.py
diff --git a/dingdan/migrations/0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi.py b/orders/migrations/0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi.py
similarity index 100%
rename from dingdan/migrations/0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi.py
rename to orders/migrations/0009_dingdanpingtai_clkf_dingdanpingtai_jjjs_biaoshi.py
diff --git a/dingdan/migrations/0010_remove_dingdanpingtai_clkf_dingdan_clkf.py b/orders/migrations/0010_remove_dingdanpingtai_clkf_dingdan_clkf.py
similarity index 100%
rename from dingdan/migrations/0010_remove_dingdanpingtai_clkf_dingdan_clkf.py
rename to orders/migrations/0010_remove_dingdanpingtai_clkf_dingdan_clkf.py
diff --git a/dingdan/migrations/0011_dingdan_tkly.py b/orders/migrations/0011_dingdan_tkly.py
similarity index 100%
rename from dingdan/migrations/0011_dingdan_tkly.py
rename to orders/migrations/0011_dingdan_tkly.py
diff --git a/dingdan/migrations/0012_dingdan_wechat_transaction_id_and_more.py b/orders/migrations/0012_dingdan_wechat_transaction_id_and_more.py
similarity index 100%
rename from dingdan/migrations/0012_dingdan_wechat_transaction_id_and_more.py
rename to orders/migrations/0012_dingdan_wechat_transaction_id_and_more.py
diff --git a/dingdan/migrations/0013_alter_chufajilu_qingqiuid.py b/orders/migrations/0013_alter_chufajilu_qingqiuid.py
similarity index 100%
rename from dingdan/migrations/0013_alter_chufajilu_qingqiuid.py
rename to orders/migrations/0013_alter_chufajilu_qingqiuid.py
diff --git a/dingdan/migrations/0014_dingdan_dispatch_type_dingdan_partner_club_id.py b/orders/migrations/0014_dingdan_dispatch_type_dingdan_partner_club_id.py
similarity index 100%
rename from dingdan/migrations/0014_dingdan_dispatch_type_dingdan_partner_club_id.py
rename to orders/migrations/0014_dingdan_dispatch_type_dingdan_partner_club_id.py
diff --git a/dingdan/migrations/0015_presettlement_orderdashouhistory_and_more.py b/orders/migrations/0015_presettlement_orderdashouhistory_and_more.py
similarity index 100%
rename from dingdan/migrations/0015_presettlement_orderdashouhistory_and_more.py
rename to orders/migrations/0015_presettlement_orderdashouhistory_and_more.py
diff --git a/dingdan/migrations/0016_alter_presettlement_dingdan_id_and_more.py b/orders/migrations/0016_alter_presettlement_dingdan_id_and_more.py
similarity index 100%
rename from dingdan/migrations/0016_alter_presettlement_dingdan_id_and_more.py
rename to orders/migrations/0016_alter_presettlement_dingdan_id_and_more.py
diff --git a/dingdan/migrations/0017_dingdanpingtai_dianpu_id_and_more.py b/orders/migrations/0017_dingdanpingtai_dianpu_id_and_more.py
similarity index 100%
rename from dingdan/migrations/0017_dingdanpingtai_dianpu_id_and_more.py
rename to orders/migrations/0017_dingdanpingtai_dianpu_id_and_more.py
diff --git a/dingdan/migrations/0018_dingdanshangjia_partner_yonghu_id.py b/orders/migrations/0018_dingdanshangjia_partner_yonghu_id.py
similarity index 100%
rename from dingdan/migrations/0018_dingdanshangjia_partner_yonghu_id.py
rename to orders/migrations/0018_dingdanshangjia_partner_yonghu_id.py
diff --git a/dingdan/migrations/0019_dingdanpingtai_shangji_dianpu_id_and_more.py b/orders/migrations/0019_dingdanpingtai_shangji_dianpu_id_and_more.py
similarity index 100%
rename from dingdan/migrations/0019_dingdanpingtai_shangji_dianpu_id_and_more.py
rename to orders/migrations/0019_dingdanpingtai_shangji_dianpu_id_and_more.py
diff --git a/dingdan/migrations/0020_fadanfenhong_fadan.py b/orders/migrations/0020_fadanfenhong_fadan.py
similarity index 100%
rename from dingdan/migrations/0020_fadanfenhong_fadan.py
rename to orders/migrations/0020_fadanfenhong_fadan.py
diff --git a/dingdan/migrations/0021_fadanshensutupian.py b/orders/migrations/0021_fadanshensutupian.py
similarity index 100%
rename from dingdan/migrations/0021_fadanshensutupian.py
rename to orders/migrations/0021_fadanshensutupian.py
diff --git a/dingdan/migrations/0022_fadan_partner_club_id_fadan_shenqingren_shenfen.py b/orders/migrations/0022_fadan_partner_club_id_fadan_shenqingren_shenfen.py
similarity index 100%
rename from dingdan/migrations/0022_fadan_partner_club_id_fadan_shenqingren_shenfen.py
rename to orders/migrations/0022_fadan_partner_club_id_fadan_shenqingren_shenfen.py
diff --git a/dingdan/migrations/0023_fadanshensutupian_yongtu.py b/orders/migrations/0023_fadanshensutupian_yongtu.py
similarity index 100%
rename from dingdan/migrations/0023_fadanshensutupian_yongtu.py
rename to orders/migrations/0023_fadanshensutupian_yongtu.py
diff --git a/dingdan/migrations/0024_fadan_chulizhe_fadan_chulizhe_shenfen.py b/orders/migrations/0024_fadan_chulizhe_fadan_chulizhe_shenfen.py
similarity index 100%
rename from dingdan/migrations/0024_fadan_chulizhe_fadan_chulizhe_shenfen.py
rename to orders/migrations/0024_fadan_chulizhe_fadan_chulizhe_shenfen.py
diff --git a/dingdan/migrations/0025_alter_fadan_shenqingren_shenfen.py b/orders/migrations/0025_alter_fadan_shenqingren_shenfen.py
similarity index 100%
rename from dingdan/migrations/0025_alter_fadan_shenqingren_shenfen.py
rename to orders/migrations/0025_alter_fadan_shenqingren_shenfen.py
diff --git a/dingdan/migrations/0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more.py b/orders/migrations/0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more.py
similarity index 100%
rename from dingdan/migrations/0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more.py
rename to orders/migrations/0026_fadanfenhonglilv_alter_fadan_shenqingren_shenfen_and_more.py
diff --git a/dingdan/migrations/0027_dingdanpeihu.py b/orders/migrations/0027_dingdanpeihu.py
similarity index 100%
rename from dingdan/migrations/0027_dingdanpeihu.py
rename to orders/migrations/0027_dingdanpeihu.py
diff --git a/dingdan/migrations/0028_dingdanshangjia_shangyige_dingdan_id.py b/orders/migrations/0028_dingdanshangjia_shangyige_dingdan_id.py
similarity index 100%
rename from dingdan/migrations/0028_dingdanshangjia_shangyige_dingdan_id.py
rename to orders/migrations/0028_dingdanshangjia_shangyige_dingdan_id.py
diff --git a/dingdan/migrations/0029_fadanfenhong_is_individual_rate.py b/orders/migrations/0029_fadanfenhong_is_individual_rate.py
similarity index 100%
rename from dingdan/migrations/0029_fadanfenhong_is_individual_rate.py
rename to orders/migrations/0029_fadanfenhong_is_individual_rate.py
diff --git a/dingdan/migrations/0030_fadan_shenqingren_fenhong_jine.py b/orders/migrations/0030_fadan_shenqingren_fenhong_jine.py
similarity index 100%
rename from dingdan/migrations/0030_fadan_shenqingren_fenhong_jine.py
rename to orders/migrations/0030_fadan_shenqingren_fenhong_jine.py
diff --git a/dingdan/migrations/0031_dingdan_guanshi_fencheng.py b/orders/migrations/0031_dingdan_guanshi_fencheng.py
similarity index 100%
rename from dingdan/migrations/0031_dingdan_guanshi_fencheng.py
rename to orders/migrations/0031_dingdan_guanshi_fencheng.py
diff --git a/houtai/migrations/__init__.py b/orders/migrations/__init__.py
similarity index 100%
rename from houtai/migrations/__init__.py
rename to orders/migrations/__init__.py
diff --git a/dingdan/models.py b/orders/models.py
similarity index 99%
rename from dingdan/models.py
rename to orders/models.py
index f9c89da..ceee486 100644
--- a/dingdan/models.py
+++ b/orders/models.py
@@ -1,5 +1,6 @@
from django.db import models
+
class Dingdan(models.Model):
"""订单主表"""
# 核心编号:唯一且不可为空[citation:3][citation:9]
diff --git a/dingdan/tongzhi_tasks.py b/orders/notice_tasks.py
similarity index 94%
rename from dingdan/tongzhi_tasks.py
rename to orders/notice_tasks.py
index 874c4b7..c250246 100644
--- a/dingdan/tongzhi_tasks.py
+++ b/orders/notice_tasks.py
@@ -1,71 +1,71 @@
-# dingdan/tongzhi_tasks.py
-import logging
-from concurrent.futures import ThreadPoolExecutor, as_completed
-from a_long_dianjing.celery_app import app
-from yonghu.models import OfficialAccountUser
-from utils.weixin_broadcast import WeixinBroadcastSender
-
-logger = logging.getLogger('weixin_broadcast')
-
-
-@app.task(bind=True, max_retries=3, default_retry_delay=60)
-def dingdan_guangbo(self, order_info):
- """
- 异步并发广播订单模板消息给所有关注服务号的用户
- order_info 字典字段示例:
- {
- 'dingdan_id': 'PT123456',
- 'game_type': '王者荣耀',
- 'amount': '88.00',
- 'order_desc': '星耀上王者'
- }
- """
- sender = WeixinBroadcastSender()
- template_data = sender.build_order_message_data(order_info)
- order_id = order_info.get('dingdan_id')
-
- # 查询所有已关注用户
- try:
- openids = list(
- OfficialAccountUser.objects
- .filter(is_subscribed=True)
- .values_list('official_openid', flat=True)
- )
- except Exception as e:
- logger.error(f'查询订阅用户失败: {e}')
- raise self.retry(exc=e)
-
- if not openids:
- return {'success': True, 'msg': '无订阅用户'}
-
- total = len(openids)
- logger.info(f'📊 Celery 广播订单 {order_id} 开始,目标人数: {total}')
-
- success = 0
- fail = 0
-
- # 20 个线程并发发送,微信接口耗时 200-500ms,1000 人约 15 秒完成
- with ThreadPoolExecutor(max_workers=20) as executor:
- future_map = {
- executor.submit(sender.send_to_user, openid, template_data, order_id): openid
- for openid in openids
- }
- for future in as_completed(future_map):
- openid = future_map[future]
- try:
- res = future.result(timeout=30)
- if res.get('success'):
- success += 1
- else:
- fail += 1
- except Exception as e:
- fail += 1
- logger.error(f'发送异常 {openid[:10]}...: {e}')
-
- logger.info(f'🏁 广播完成:成功 {success},失败 {fail},总计 {total}')
- return {
- 'success': True,
- 'total': total,
- 'success_count': success,
- 'fail_count': fail
+# dingdan/notice_tasks.py
+import logging
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from a_long_dianjing.celery_app import app
+from users.models import OfficialAccountUser
+from utils.weixin_broadcast import WeixinBroadcastSender
+
+logger = logging.getLogger('weixin_broadcast')
+
+
+@app.task(bind=True, max_retries=3, default_retry_delay=60)
+def dingdan_guangbo(self, order_info):
+ """
+ 异步并发广播订单模板消息给所有关注服务号的用户
+ order_info 字典字段示例:
+ {
+ 'dingdan_id': 'PT123456',
+ 'game_type': '王者荣耀',
+ 'amount': '88.00',
+ 'order_desc': '星耀上王者'
+ }
+ """
+ sender = WeixinBroadcastSender()
+ template_data = sender.build_order_message_data(order_info)
+ order_id = order_info.get('dingdan_id')
+
+ # 查询所有已关注用户
+ try:
+ openids = list(
+ OfficialAccountUser.objects
+ .filter(is_subscribed=True)
+ .values_list('official_openid', flat=True)
+ )
+ except Exception as e:
+ logger.error(f'查询订阅用户失败: {e}')
+ raise self.retry(exc=e)
+
+ if not openids:
+ return {'success': True, 'msg': '无订阅用户'}
+
+ total = len(openids)
+ logger.info(f'📊 Celery 广播订单 {order_id} 开始,目标人数: {total}')
+
+ success = 0
+ fail = 0
+
+ # 20 个线程并发发送,微信接口耗时 200-500ms,1000 人约 15 秒完成
+ with ThreadPoolExecutor(max_workers=20) as executor:
+ future_map = {
+ executor.submit(sender.send_to_user, openid, template_data, order_id): openid
+ for openid in openids
+ }
+ for future in as_completed(future_map):
+ openid = future_map[future]
+ try:
+ res = future.result(timeout=30)
+ if res.get('success'):
+ success += 1
+ else:
+ fail += 1
+ except Exception as e:
+ fail += 1
+ logger.error(f'发送异常 {openid[:10]}...: {e}')
+
+ logger.info(f'🏁 广播完成:成功 {success},失败 {fail},总计 {total}')
+ return {
+ 'success': True,
+ 'total': total,
+ 'success_count': success,
+ 'fail_count': fail
}
\ No newline at end of file
diff --git a/orders/signals.py b/orders/signals.py
new file mode 100644
index 0000000..5055f15
--- /dev/null
+++ b/orders/signals.py
@@ -0,0 +1,102 @@
+"""
+阿龙电竞 - 订单信号处理器(生产稳定版)
+经过验证,100%可靠
+"""
+
+import sys
+import importlib
+import logging
+from django.db.models.signals import pre_save
+from django.dispatch import receiver
+from django.conf import settings
+from django.utils import timezone
+
+logger = logging.getLogger(__name__)
+
+# 🔧 关键修复:确保redis模块正确加载
+def ensure_redis_loaded():
+ """确保redis模块正确加载,避免导入冲突"""
+ if 'redis' not in sys.modules:
+ # 如果redis模块未加载,尝试导入
+ try:
+ import redis
+ #logger.debug("✅ Redis模块正常加载")
+ except ImportError:
+ #logger.warning("⚠️ Redis模块未安装,尝试自动安装")
+ import subprocess
+ subprocess.call([sys.executable, "-m", "pip", "install", "redis==4.5.4", "-q"])
+ import redis
+
+ # 确保redis.Redis属性存在
+ import redis
+ if redis.Redis is None:
+ #logger.warning("⚠️ Redis.Redis属性为None,强制重新加载模块")
+ importlib.reload(redis)
+
+ return redis
+
+# 预加载redis模块
+ensure_redis_loaded()
+
+@receiver(pre_save, sender='dingdan.Dingdan')
+def handle_order_status_8(sender, instance, **kwargs):
+ """
+ 订单状态变为8时,提交定时任务
+ """
+ # 跳过新增的订单
+ if instance.pk is None:
+ return
+
+ try:
+ # 动态导入,避免循环依赖
+ from orders.models import Dingdan
+
+ # 获取旧状态
+ old = Dingdan.objects.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first()
+
+ # 状态从 非8 变为 8
+ if old != 8 and instance.zhuangtai == 8:
+ #logger.info(f"📅 订单 {instance.dingdan_id} 状态变为8,提交定时任务")
+
+ # 设置时间标记
+ instance.status_8_time = timezone.now()
+ instance.pending_dispatch = True
+
+ # 使用配置中的超时时间
+ expire_seconds = getattr(settings, 'ORDER_EXPIRE_SECONDS', 48 * 60 * 60)
+
+ # 提交Celery任务
+ try:
+ from orders.tasks import process_expired_order
+
+ task_result = process_expired_order.apply_async(
+ args=[instance.dingdan_id],
+ countdown=expire_seconds,
+ queue='order_tasks',
+ priority=9
+ )
+
+ #logger.info(f"✅ 任务提交成功,订单: {instance.dingdan_id}, 任务ID: {task_result.id}")
+
+ # 保存任务信息
+ instance.auto_task_id = task_result.id
+ instance.auto_expire_at = timezone.now() + timezone.timedelta(seconds=expire_seconds)
+
+ except Exception as e:
+ logger.error(f"❌ 任务提交失败: {e}")
+ instance.pending_dispatch = True
+ instance.auto_task_id = f"failed_{timezone.now().timestamp()}"
+
+ # 状态从 8 变为 非8
+ elif old == 8 and instance.zhuangtai != 8:
+ instance.status_8_time = None
+ instance.pending_dispatch = False
+ instance.auto_task_id = ''
+ logger.info(f"📅 订单 {instance.dingdan_id} 状态离开8")
+
+ except Exception as e:
+ logger.error(f"信号处理失败: {e}")
+
+
+
+
diff --git a/dingdan/tasks.py b/orders/tasks.py
similarity index 96%
rename from dingdan/tasks.py
rename to orders/tasks.py
index c079059..8a71d45 100644
--- a/dingdan/tasks.py
+++ b/orders/tasks.py
@@ -14,8 +14,8 @@ from decimal import Decimal
import logging
# 导入模型 - 确保路径正确
-from dingdan.models import Dingdan, DingdanShangjia
-from yonghu.models import UserMain, UserDashou, UserShangjia
+from orders.models import Dingdan
+from users.models import UserMain, UserDashou, UserShangjia
from utils.celery_utils import safe_decimal_operation, log_task_execution, rollback_on_failure
logger = logging.getLogger(__name__)
@@ -25,9 +25,6 @@ logger = logging.getLogger(__name__)
-
-
-
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def process_expired_order(self, dingdan_id):
"""
@@ -43,13 +40,6 @@ def process_expired_order(self, dingdan_id):
order = Dingdan.objects.select_for_update().get(
dingdan_id=dingdan_id
)
-
-
-
- # 其余代码不变...
- # ... 保持原有代码
-
-
# 再次检查订单状态是否为8
if order.zhuangtai != 8:
@@ -124,7 +114,7 @@ def process_expired_order(self, dingdan_id):
if fadan_pingtai == 1:
try:
- from dingdan.utils import update_platform_profit # 导入函数
+ from orders.utils import update_platform_profit # 导入函数
update_platform_profit(dingdan_id)
except Exception as e:
# 平台收益更新失败不影响主流程,但记录日志
diff --git a/dingdan/tests.py b/orders/tests.py
similarity index 100%
rename from dingdan/tests.py
rename to orders/tests.py
diff --git a/dingdan/urls.py b/orders/urls.py
similarity index 99%
rename from dingdan/urls.py
rename to orders/urls.py
index ef2c3d9..9a39f63 100644
--- a/dingdan/urls.py
+++ b/orders/urls.py
@@ -65,8 +65,6 @@ urlpatterns = [
-
-
path('partner_sync_order', csrf_exempt(PartnerSyncOrderView.as_view()), name='东道主派单接口'),
path('partner_check_claim', csrf_exempt(PartnerCheckClaimView.as_view()), name='对方抢单询问接口'),
@@ -86,6 +84,4 @@ urlpatterns = [
-
-
]
\ No newline at end of file
diff --git a/dingdan/utils.py b/orders/utils.py
similarity index 94%
rename from dingdan/utils.py
rename to orders/utils.py
index 0c64ed8..69d97ba 100644
--- a/dingdan/utils.py
+++ b/orders/utils.py
@@ -1,21 +1,25 @@
-
import threading
import requests
-
-from django.conf import settings
-from peizhi.models import Club
-from shangpin.models import ShangpinLeixing
-from dingdan.models import Dingdan # 根据实际路径导入
-
+import time
import logging
from datetime import date
+from decimal import Decimal
+from django.conf import settings
from django.db import transaction
from django.db.models import F
-from peizhi.models import DailyDispatchStat # 假设模型在 peizhi 应用中
+
+from config.models import Club, DailyDispatchStat, DailyIncomeStat, DailyPayoutStat
+from orders.models import Dingdan, DingdanPingtai, DingdanShangjia, Lilubiao
+from products.models import ShangpinLeixing, Gsfenhong
+from users.models import UserDashou, UserGuanshi
+
+from backend.utils import update_guanshi_daily_by_action
logger = logging.getLogger(__name__)
+
+
# 可选:线程池限制(避免大量线程同时创建,建议使用 Celery,这里简单用线程)
THREAD_POOL_SIZE = 10
_semaphore = threading.Semaphore(THREAD_POOL_SIZE)
@@ -149,7 +153,6 @@ def _send_to_single_partner(dingdan, club):
logger.error(f"同步订单异常(尝试 {attempt}/3): {e}")
if attempt < 3:
- import time
time.sleep(2 ** attempt) # 指数退避
else:
logger.error(f"同步订单 {dingdan.dingdan_id} 到俱乐部 {club.club_id} 最终失败")
@@ -158,7 +161,6 @@ def _send_to_single_partner(dingdan, club):
# 获取下单方ID(根据订单类型)
def get_order_user_id(order):
- from dingdan.models import DingdanPingtai, DingdanShangjia
if order.fadan_pingtai == 1:
try:
ext = DingdanPingtai.objects.get(dingdan=order)
@@ -176,18 +178,6 @@ def get_order_user_id(order):
-# dingdan/utils/dispatch_stat.py
-import logging
-
-import logging
-from datetime import date
-from decimal import Decimal
-from django.db import transaction
-from django.db.models import F
-from peizhi.models import DailyDispatchStat # 请根据实际模型位置修改导入路径
-
-logger = logging.getLogger(__name__)
-
def update_daily_dispatch_stat(direction, partner_club_id,
dispatch_amount=None, dispatch_count=1,
@@ -255,9 +245,6 @@ def update_daily_dispatch_stat(direction, partner_club_id,
-from peizhi.models import DailyIncomeStat
-
-logger = logging.getLogger(__name__)
def update_daily_income(amount):
@@ -295,9 +282,6 @@ def update_daily_income(amount):
-from peizhi.models import DailyPayoutStat
-
-logger = logging.getLogger(__name__)
def update_daily_payout(amount):
"""
@@ -335,10 +319,6 @@ def update_daily_payout(amount):
-from decimal import Decimal
-
-from dingdan.models import Lilubiao
-
def calc_shangjia_order_fencheng(jine):
"""
@@ -374,8 +354,6 @@ def settle_shangjia_order_guanshi_fenhong(order, dashou_id):
"""
商家订单结单时结算管事分红(幂等,失败不抛异常阻断主流程)。
"""
- from shangpin.models import Gsfenhong
- from yonghu.models import UserDashou, UserGuanshi
if getattr(order, 'fadan_pingtai', None) != 2:
return
@@ -433,7 +411,6 @@ def settle_shangjia_order_guanshi_fenhong(order, dashou_id):
return
try:
- from houtai.utils import update_guanshi_daily_by_action
update_guanshi_daily_by_action(
yonghuid=guanshi_id,
action=3,
diff --git a/dingdan/views.py b/orders/views.py
similarity index 90%
rename from dingdan/views.py
rename to orders/views.py
index 3dee938..6d5a984 100644
--- a/dingdan/views.py
+++ b/orders/views.py
@@ -1,98 +1,84 @@
+# ==================== 标准库 ====================
import hmac
import os
-from dingdan.utils import sync_order_to_partners
-from django.db.models import F
-from django.shortcuts import render
-from rest_framework.permissions import AllowAny # 新增这行
-from rest_framework.throttling import AnonRateThrottle
-from dingdan.utils import sync_order_to_partners
-# dingdan/views.py
-import traceback
+import json
import time
import random
+import string
+import uuid
+import traceback
+import threading
+import requests
import hashlib
import xml.etree.ElementTree as ET
-import requests
-import uuid
+import xmltodict
+import logging
from decimal import Decimal
-from django.db import transaction
+from datetime import datetime
+
from django.conf import settings
+from django.db import models, transaction
+from django.db.models import F, Q, Count, OuterRef, Subquery, Prefetch
+from django.core.cache import cache
+from django.core.paginator import Paginator
+from django.core.exceptions import ObjectDoesNotExist
+from django.shortcuts import render
+from django.http import HttpResponse
+from django.utils import timezone
+
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 JSONParser
+from rest_framework.throttling import AnonRateThrottle
+from rest_framework_simplejwt.authentication import JWTAuthentication
+
+from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
+from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
+from tencentcloud.sts.v20180813 import sts_client, models
-from shangpin.models import Shangpin, ShangpinLeixing, Huiyuangoumai
-from dingdan.models import Dingdan, DingdanPingtai, Lilubiao, Pingfen, DingdanShangjia, Chufajilu, Liaotian, Chufatupian
from utils.weixin_broadcast import WeixinBroadcastSender
-from yonghu.models import UserMain, UserDashou, UserShangjia
+from utils.goeasy_service import GoEasyService
+from utils.chat_utils import _send_group_message, _subscribe_users_to_group, establish_order_chat
+from utils.fadan_utils import check_fadan_qiangdan_eligible
-from shangdian.utils import calculate_pingtai_and_dianpu_shouyi,validate_shangpin_and_dianpu
-from shangpin.utils import update_shangpin_daily_stat
-from shangdian.utils import update_dianpu_daily_stat # 注意:您之前写的店铺统计方法名是 update_dianpu_daily_stat
+from orders.utils import (
+ update_platform_profit, calc_shangjia_order_fencheng, sync_order_to_partners,
+ update_daily_dispatch_stat, update_daily_income, update_daily_payout,
+ settle_shangjia_order_guanshi_fenhong
+)
+from shop.utils import calculate_pingtai_and_dianpu_shouyi, validate_shangpin_and_dianpu, update_dianpu_daily_stat
+from products.utils import update_shangpin_daily_stat
+from backend.utils import update_shangjia_daily, update_dashou_daily_by_action
+from rank.services import record_dashou_biaoxian
+from rank.utils import check_dashou_biaoqian_required
-from houtai.utils import update_dashou_daily_by_action
-
-# dingdan/views.py (CreateOrderView)
-
-
-# dingdan/views.py (WechatPayNotifyView)
-
-import threading
-import hashlib
-import xml.etree.ElementTree as ET
-from decimal import Decimal
-from django.db import transaction, models
-from django.http import HttpResponse
-from rest_framework.views import APIView
-from rest_framework.permissions import AllowAny
-from rest_framework.throttling import AnonRateThrottle
-import logging
-from rest_framework import permissions
-
-from shangpin.models import Shangpin
-
-from yonghu.models import UserMain, UserBoss
-
-
-from django.db import transaction
-from django.http import HttpResponse
-from rest_framework.views import APIView
-
-from rest_framework.throttling import AnonRateThrottle
-from rest_framework.permissions import AllowAny
-
-from peizhi.models import Szjilu
+from orders.notice_tasks import dingdan_guangbo
+from .models import (
+ Dingdan, DingdanShangjia, DingdanPingtai, Dashoutupian,
+ Chufajilu, Chufatupian, Fadan, FadanFenhong, Liaotian
+)
+from orders.models import (
+ Lilubiao, Pingfen, PreSettlement, Tuikuanjilu, CrossPlatformOrderData
+)
+from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai
+from users.models import UserMain, UserDashou, UserShangjia, UserBoss
+from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao
+from config.models import (
+ Szjilu, Club, ClubConfig, ShangjiaLianjie, OrderTypeMapping
+)
logger = logging.getLogger(__name__)
-# dingdan/views.py
-import threading
-import hashlib
-import logging
-import traceback
-import xml.etree.ElementTree as ET
-from decimal import Decimal
-from django.db import transaction, models
-from django.http import HttpResponse
-from django.conf import settings
-from rest_framework.views import APIView
-from rest_framework.permissions import AllowAny
-from rest_framework.throttling import AnonRateThrottle
-
-# 假设这些模型已正确导入
-from dingdan.models import Dingdan, DingdanPingtai
-from shangpin.models import Shangpin
-from yonghu.models import UserMain, UserBoss
-from peizhi.models import Szjilu
-
logger = logging.getLogger(__name__)
-# dingdan/views.py (PaymentFailView)
+
class PaymentFailView(APIView):
"""
@@ -130,32 +116,6 @@ class PaymentFailView(APIView):
return Response({'code': 99, 'msg': f'删除订单失败: {str(e)}', 'data': None})
-import string
-import requests
-import xmltodict
-import logging
-from decimal import Decimal
-from django.db import transaction, models
-from django.core.cache import cache
-from django.conf import settings
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import permissions, status
-from rest_framework.parsers import JSONParser
-
-from yonghu.models import UserMain, UserBoss
-from shangpin.models import Shangpin
-from dingdan.models import Dingdan, DingdanPingtai
-from peizhi.models import Szjilu
-
-logger = logging.getLogger(__name__)
-
-
-from dingdan.utils import update_daily_income
-import logging
-import traceback
-
-logger = logging.getLogger(__name__)
class WechatPayNotifyView(APIView):
"""微信支付回调接口 - 接收微信官方支付结果通知(含详细调试日志)"""
@@ -359,9 +319,6 @@ class WechatPayNotifyView(APIView):
# 跨平台同步
sync_order_to_partners(dingdan.dingdan_id)
- #🟢【7. 新增】支付成功后的广播通知(异步,不阻塞主流程)
- from dingdan.tongzhi_tasks import dingdan_guangbo
-
# 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用)
order_info = {
'dingdan_id': dingdan.dingdan_id,
@@ -430,7 +387,6 @@ class WechatPayNotifyView(APIView):
try:
def task():
try:
- from utils.weixin_broadcast import WeixinBroadcastSender # 请根据实际路径修改
order_info = {
'dingdan_id': dingdan.dingdan_id,
'game_type': self._get_game_type_name(dingdan.leixing_id),
@@ -457,7 +413,6 @@ class WechatPayNotifyView(APIView):
def _get_game_type_name(self, leixing_id):
try:
- from shangpin.models import ShangpinLeixing
gt = ShangpinLeixing.objects.filter(id=leixing_id).first()
return gt.jieshao if gt else '游戏订单'
except Exception as e:
@@ -465,7 +420,7 @@ class WechatPayNotifyView(APIView):
return '游戏订单'
-from django.core.cache import cache
+
class PaymentVerifyView(APIView):
"""
订单支付成功复查接口 - 前端支付成功后调用,确认订单状态
@@ -718,30 +673,6 @@ class PaymentVerifyView(APIView):
return ''.join(xml)
-import os
-from django.db.models import F
-from django.shortcuts import render
-from rest_framework.permissions import AllowAny # 新增这行
-from rest_framework.throttling import AnonRateThrottle
-# dingdan/views.py
-import time
-import random
-import hashlib
-import xml.etree.ElementTree as ET
-import requests
-import uuid
-from decimal import Decimal
-from django.db import transaction
-from django.conf import settings
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import permissions, status
-from tencentcloud.common.profile.client_profile import ClientProfile
-from tencentcloud.common.profile.http_profile import HttpProfile
-
-from shangpin.models import Shangpin, ShangpinLeixing, Huiyuangoumai
-from dingdan.models import Dingdan, DingdanPingtai, Lilubiao, Pingfen,Chufatupian, DingdanShangjia, Chufajilu, Liaotian
-from yonghu.models import UserMain, UserDashou, UserShangjia
class CreateOrderView(APIView):
@@ -981,7 +912,6 @@ class CreateOrderView(APIView):
raise Exception('微信支付配置不完整')
# 生成随机字符串
- import string
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# 金额(单位:分)
@@ -1095,23 +1025,6 @@ class CreateOrderView(APIView):
raise e
-# dingdan/views.py
-
-
-# dingdan/views.py
-
-
-from yonghu.models import UserMain
-
-# dingdan/views.py
-
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from django.db.models import Q
-
-from .models import Dingdan, DingdanPingtai
-
class DingdanHuoquView(APIView):
"""订单获取接口"""
@@ -1225,15 +1138,7 @@ class DingdanHuoquView(APIView):
})
-# dingdan/views.py
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from django.db.models import Q
-import json
-
-from .models import Dingdan, DingdanPingtai, Dashoutupian
class DingdanXiangqingView(APIView):
@@ -1281,7 +1186,6 @@ class DingdanXiangqingView(APIView):
if not pingtai_kuozhan:
# 如果不是平台订单,检查商家订单
- from .models import DingdanShangjia
shangjia_kuozhan = DingdanShangjia.objects.filter(
shangjia_id=yonghuid,
dingdan__dingdan_id=dingdan_id
@@ -1593,7 +1497,6 @@ class JiedanView(APIView):
# 12. 更新平台收益流水(仅在平台发单时)
if dingdan.fadan_pingtai == 1:
try:
- from dingdan.utils import update_platform_profit # 导入函数
update_platform_profit(dingdan_id)
except Exception as e:
# 平台收益更新失败不影响主流程,但记录日志
@@ -1623,7 +1526,6 @@ class JiedanView(APIView):
"""
try:
# 1. 获取我方俱乐部ID
- from .models import CrossPlatformOrderData
my_club_config = ClubConfig.objects.filter(is_self=1).first()
if not my_club_config:
logger.error("未配置我方俱乐部信息,无法通知对方")
@@ -1670,8 +1572,6 @@ class JiedanView(APIView):
logger.error(f"通知对方平台异常: {str(e)}")
-from dengji.models import DashouBiaoxian
-from dengji.services import record_dashou_biaoxian
class DingdanXiangqingView2(APIView):
@@ -1932,7 +1832,6 @@ class JiedanView2(APIView):
# 13. 平台收益流水(仅平台发单)
if order.fadan_pingtai == 1:
try:
- from dingdan.utils import update_platform_profit # 导入函数
update_platform_profit(dingdan_id)
except Exception as e:
logger.error(f"平台收益更新失败: {e}")
@@ -1957,11 +1856,6 @@ class JiedanView2(APIView):
return Response({'code': 9, 'msg': '服务器内部错误,结单失败', 'data': None})
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from django.db.models import Prefetch
-from dengji.models import Chenghao
class ShangpinLeixingView(APIView):
@@ -2022,254 +1916,8 @@ class ShangpinLeixingView(APIView):
except Exception as e:
logger.error(f"获取商品类型失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
-
-import threading
-from utils.weixin_broadcast import WeixinBroadcastSender
-from dengji.models import DingdanBiaoqian,Chenghao
-
-
-import os
-import threading
-import logging
-from decimal import Decimal
-from django.db import transaction
-from django.db.models import F
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework import status
-
-from yonghu.models import UserMain, UserShangjia, UserDashou
-
-from houtai.utils import update_shangjia_daily
-
-
-# 导入 Celery 广播任务
-from dingdan.tongzhi_tasks import dingdan_guangbo
-
-logger = logging.getLogger(__name__)
-
-
-"""class ShangjiaPaifaView(APIView):
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- logger.info(f"商家派单请求: user_id={request.user.yonghuid}, data={request.data}")
-
- try:
- data = request.data
-
- # ----- 1. 获取原始参数(保持原类型)-----
- shangpin_type_id = data.get('shangpinTypeId')
- huiyuan_id = data.get('huiyuanId')
- yaoqiuleixing = data.get('yaoqiuleixing')
- dingdan_jieshao_raw = data.get('dingdanJieshao', '')
- dingdan_beizhu_raw = data.get('dingdanBeizhu', '')
- laoban_name_raw = data.get('laobanName', '')
- zhiding_uid_raw = data.get('zhidingUid', '')
- jiage_str = data.get('jiage')
-
- label_id = data.get('labelId') # 整数或 None
- commission_enabled = data.get('commissionEnabled', False)
- commission_value_raw = data.get('commissionValue')
-
- # ----- 2. 字符串字段安全处理 -----
- dingdan_jieshao = str(dingdan_jieshao_raw).strip()
- dingdan_beizhu = str(dingdan_beizhu_raw).strip()
- laoban_name = str(laoban_name_raw).strip()
- zhiding_uid = str(zhiding_uid_raw).strip()
- commission_value_str = str(commission_value_raw).strip() if commission_value_raw is not None else ''
-
- # ----- 3. 必填校验 -----
- if not all([shangpin_type_id, huiyuan_id, dingdan_jieshao, laoban_name, jiage_str]):
- logger.warning(f"参数缺失: {request.data}")
- return Response({'code': 400, 'msg': '缺少必填参数'})
-
- # ----- 4. 价格验证 -----
- try:
- jiage = float(jiage_str)
- if jiage < 1 or jiage > 10000:
- return Response({'code': 400, 'msg': '价格需在1-10000元之间'})
- if '.' in str(jiage_str) and len(str(jiage_str).split('.')[1]) > 2:
- return Response({'code': 400, 'msg': '最多支持两位小数'})
- except ValueError:
- return Response({'code': 400, 'msg': '价格格式错误'})
-
- # ----- 5. 商品类型 -----
- try:
- shangpin_type = ShangpinLeixing.objects.get(id=shangpin_type_id)
- except ShangpinLeixing.DoesNotExist:
- logger.error(f"商品类型不存在: {shangpin_type_id}")
- return Response({'code': 400, 'msg': '商品类型不存在'})
-
- # ----- 6. 押金 / 会员模式决定(修正版)-----
- # 先根据商品类型中的配置填充默认值
- final_yaoqiu_type = shangpin_type.yaoqiuleixing if shangpin_type.yaoqiuleixing else 1
- final_yongjin = shangpin_type.yongjin if shangpin_type.yongjin else None
- final_huiyuan_id = shangpin_type.huiyuan_id if shangpin_type.huiyuan_id else ''
-
- # 如果前端明确开启了佣金且金额大于0,则覆盖为押金模式
- if commission_enabled and commission_value_str:
- try:
- cv = float(commission_value_str)
- if cv > 0:
- final_yaoqiu_type = 2
- final_yongjin = Decimal(str(cv))
- final_huiyuan_id = ''
- logger.info(f"前端开启押金模式: 佣金={cv}")
- else:
- # 佣金金额为0,视为无效,保持商品类型默认
- logger.warning("前端传佣金为0,无效,使用商品类型默认")
- except ValueError:
- logger.warning("佣金格式错误,使用商品类型默认")
- else:
- logger.info(f"使用商品类型默认模式: 类型={final_yaoqiu_type}, huiyuan_id={final_huiyuan_id}, yongjin={final_yongjin}")
-
- # ----- 7. 称号验证(label_id 是整数或 None)-----
- chenghao_obj = None
- if label_id is not None:
- try:
- chenghao_obj = Chenghao.objects.get(id=int(label_id))
- logger.info(f"订单将关联称号: {chenghao_obj.mingcheng}")
- except (Chenghao.DoesNotExist, ValueError, TypeError):
- logger.warning(f"称号不存在或ID无效: {label_id}")
- return Response({'code': 400, 'msg': '所选标签不存在'})
-
- # ----- 8. 商家信息 -----
- try:
- shangjia = request.user.shop_profile
- except UserShangjia.DoesNotExist:
- logger.warning(f"用户不是商家: {request.user.yonghuid}")
- return Response({'code': 403, 'msg': '您不是商家'})
-
- if shangjia.zhuangtai != 1:
- logger.warning(f"商家状态异常: {shangjia.zhuangtai}")
- return Response({'code': 403, 'msg': '商家状态异常'})
-
- if jiage > shangjia.yue:
- logger.warning(f"余额不足: {shangjia.yue} < {jiage}")
- return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元'})
-
- # ----- 9. 指定打手 -----
- zhiding_dashou = None
- if zhiding_uid:
- try:
- zhiding_user = UserMain.objects.get(yonghuid=zhiding_uid)
- zhiding_dashou = zhiding_user.dashou_profile
- if zhiding_dashou.zhanghaozhuangtai != 1:
- return Response({'code': 400, 'msg': '指定打手状态异常'})
- except UserMain.DoesNotExist:
- return Response({'code': 400, 'msg': '指定用户不存在'})
- except UserDashou.DoesNotExist:
- return Response({'code': 400, 'msg': '指定用户不是打手'})
-
- # ----- 10. 利率 -----
- try:
- lilu_obj = Lilubiao.objects.get(fadanpingtai='3')
- lilu = float(lilu_obj.lilu) if lilu_obj.lilu > 0 else 1
- except Lilubiao.DoesNotExist:
- lilu = 1
- dashou_fencheng = round(jiage * lilu, 2)
-
- # ----- 11. 生成订单ID -----
- def generate_dingdan_id():
- import time, random
- ts = int(time.time() * 1000)
- pid = os.getpid() % 1000
- rnd = random.randint(1000, 9999)
- return f"SJ{ts}{pid:03d}{rnd}"
-
- # ----- 12. 事务:创建订单、更新商家统计 -----
- dingdan = None
- try:
- with transaction.atomic():
- dingdan = Dingdan.objects.create(
- dingdan_id=generate_dingdan_id(),
- zhuangtai=7 if zhiding_dashou else 1,
- fadan_pingtai=2,
- jine=jiage,
- dashou_fencheng=dashou_fencheng,
- zhiding_id=zhiding_uid if zhiding_dashou else '',
- shangpin_id=0,
- leixing_id=int(shangpin_type_id),
- yaoqiuleixing=final_yaoqiu_type,
- yongjin=final_yongjin,
- huiyuan_id=final_huiyuan_id,
- jieshao=dingdan_jieshao[:2000],
- beizhu=dingdan_beizhu[:1000],
- nicheng=laoban_name[:50],
- tupian=request.user.avatar,
- user1_id=f"Sj{request.user.yonghuid}"
- )
- logger.info(f"订单主表创建: {dingdan.dingdan_id}")
-
- DingdanShangjia.objects.create(
- dingdan=dingdan,
- shangjia_id=request.user.yonghuid,
- sjnicheng=shangjia.nicheng,
- )
-
- if chenghao_obj:
- DingdanBiaoqian.objects.create(dingdan=dingdan, chenghao=chenghao_obj)
- logger.info(f"订单关联称号: {chenghao_obj.mingcheng}")
-
- # 更新商家统计
- UserShangjia.objects.filter(id=shangjia.id).update(
- fabu=F('fabu') + 1,
- yue=F('yue') - jiage,
- jinridingdan=F('jinridingdan') + 1,
- jinriliushui=F('jinriliushui') + jiage,
- jinyuedingdan=F('jinyuedingdan') + 1,
- jinyueliushui=F('jinyueliushui') + jiage,
- )
-
- update_shangjia_daily(
- yonghuid=request.user.yonghuid,
- amount=Decimal(str(jiage)),
- action=1
- )
- # 事务提交,订单已持久化
- except Exception as e:
- logger.error(f"订单创建事务失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '订单创建失败,请稍后重试'})
-
- # ========== 以下操作均在事务外,避免异步任务查不到订单 ==========
- try:
- sync_order_to_partners(dingdan.dingdan_id)
- except Exception as e:
- logger.error(f"跨平台同步失败: {e}", exc_info=True)
-
- try:
- order_info = {
- 'dingdan_id': dingdan.dingdan_id,
- 'game_type': self._get_game_type_name(dingdan.leixing_id),
- 'amount': str(dingdan.jine),
- 'order_desc': (dingdan.jieshao or '')[:50],
- }
- dingdan_guangbo.delay(order_info)
- logger.info(f"已提交广播任务: {dingdan.dingdan_id}")
- except Exception as e:
- logger.error(f"广播任务提交失败: {e}", exc_info=True)
-
- logger.info(f"商家派单成功: {dingdan.dingdan_id}, 金额={jiage}")
- return Response({'code': 200, 'msg': '派单成功', 'data': {'jine': str(dingdan.jine)}})
-
- except Exception as e:
- logger.error(f"商家派单接口未捕获异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '系统内部错误'})
-
- def _get_game_type_name(self, leixing_id):
- try:
- gt = ShangpinLeixing.objects.filter(id=leixing_id).first()
- return gt.jieshao if gt else '游戏订单'
- except Exception:
- return '游戏订单'"""
-
-
-logger = logging.getLogger(__name__)
-
class ShangjiaPaifaView(APIView):
permission_classes = [IsAuthenticated]
@@ -2390,12 +2038,10 @@ class ShangjiaPaifaView(APIView):
return Response({'code': 400, 'msg': '指定用户不是打手'})
# ----- 10. 利率(打手+管事分成,打手优先) -----
- from dingdan.utils import calc_shangjia_order_fencheng
dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage)
# ----- 11. 生成订单ID -----
def generate_dingdan_id():
- import time, random
ts = int(time.time() * 1000)
pid = os.getpid() % 1000
rnd = random.randint(1000, 9999)
@@ -2490,20 +2136,7 @@ class ShangjiaPaifaView(APIView):
except Exception:
return '游戏订单'
-# apps/dingdan/views.py
-from django.db.models import Q, Count
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from .models import Dingdan, DingdanShangjia
-import logging
-
-logger = logging.getLogger(__name__)
-
-
# views.py 中 ShangjiaDingdanHuoquView 完整改造
-
class ShangjiaDingdanHuoquView(APIView):
"""
商家订单获取接口
@@ -2563,7 +2196,7 @@ class ShangjiaDingdanHuoquView(APIView):
# 5. 状态过滤
base_qs = base_qs.filter(dingdan__zhuangtai__in=zhuangtai_list)
- # 🆕 6. 商品类型筛选
+ # 6. 商品类型筛选
if leixing_id is not None:
try:
leixing_id_int = int(leixing_id)
@@ -2572,9 +2205,8 @@ class ShangjiaDingdanHuoquView(APIView):
# 如果leixing_id不是有效整数,忽略该筛选
pass
- # 🆕 7. 模糊搜索(支持订单ID、订单介绍、游戏昵称、打手ID)
+ # 7. 模糊搜索(支持订单ID、订单介绍、游戏昵称、打手ID)
if keyword:
- from django.db.models import Q
# 订单ID精确匹配(dingdan_id有唯一索引,先用精确匹配)
dingdan_id_match = Q(dingdan__dingdan_id__exact=keyword)
# 订单介绍模糊匹配
@@ -2642,11 +2274,6 @@ class ShangjiaDingdanHuoquView(APIView):
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-from dingdan.models import Dingdan, DingdanShangjia, Dashoutupian, Fadan
-
-from .models import FadanFenhong # 注意:fadan_fenhong 可能在其他 app 中,根据实际路径调整
-
-logger = logging.getLogger(__name__)
class ShangjiaDingdanXiangqingView(APIView):
@@ -2821,14 +2448,7 @@ class ShangjiaDingdanXiangqingView(APIView):
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-from dingdan.models import Dingdan, DingdanShangjia, PreSettlement
-from dingdan.utils import update_daily_dispatch_stat
-
-
-logger = logging.getLogger(__name__)
-
-from houtai.utils import update_shangjia_daily
class ShangjiaJiesuanView(APIView):
"""商家结单接口(支持跨平台订单)"""
permission_classes = [permissions.IsAuthenticated]
@@ -3016,7 +2636,7 @@ class ShangjiaJiesuanView(APIView):
shangjia_pingjia=liuyan,
)
- # 🔥 新增:调用打手每日统计(成交)
+ # 新增:调用打手每日统计(成交)
try:
update_dashou_daily_by_action(
yonghuid=jiedan_dashou_id,
@@ -3037,7 +2657,6 @@ class ShangjiaJiesuanView(APIView):
# 商家订单管事分红(仅 fadan_pingtai=2)
if order.fadan_pingtai == 2:
- from dingdan.utils import settle_shangjia_order_guanshi_fenhong
settle_shangjia_order_guanshi_fenhong(order, jiedan_dashou_id)
# 返回成功
@@ -3070,7 +2689,6 @@ class ShangjiaJiesuanView(APIView):
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu
class ShangjiaChexiaoView(APIView):
@@ -3181,8 +2799,7 @@ class ShangjiaChexiaoView(APIView):
dingdan.save()
- # ✅ 新方法(行为驱动,更安全)
- from houtai.utils import update_shangjia_daily
+ # 行为驱动
update_shangjia_daily(
yonghuid=request.user.yonghuid,
amount=Decimal(str(dingdan.jine)),
@@ -3202,8 +2819,6 @@ class ShangjiaChexiaoView(APIView):
except Exception as e:
# 记录异常日志
- import logging
- logger = logging.getLogger(__name__)
logger.error(f"商家撤销订单失败: {str(e)}", exc_info=True)
return Response({
@@ -3332,9 +2947,6 @@ class ShangjiaTuikuanShenqingView(APIView):
# ========== 新增:如果是跨平台订单,异步通知对方平台 ==========
if dingdan.is_cross == 1:
- import threading
- import requests
- from django.conf import settings
def send_refund_notify():
url = f"{settings.PARTNER_BASE_URL}{settings.PARTNER_REFUND_NOTIFY_URL}"
@@ -3535,8 +3147,6 @@ class ShangjiaChufaShenqingView(APIView):
}, status=status.HTTP_200_OK)
except Exception as e:
- import logging
- logger = logging.getLogger(__name__)
logger.error(f"商家处罚申请失败: {str(e)}", exc_info=True)
return Response({
@@ -3546,22 +3156,8 @@ class ShangjiaChufaShenqingView(APIView):
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-# apps/dingdan/views.py
-from django.db.models import Q, OuterRef, Subquery
-from django.core.paginator import Paginator
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework_simplejwt.authentication import JWTAuthentication
-from .models import Dingdan, DingdanShangjia
-# dingdan/views.py
-from django.db.models import Q, OuterRef, Subquery, F, Prefetch
-
-from dengji.models import DingdanBiaoqian
-from dengji.models import YonghuChenghao, Chenghao
-
class DashouDingdanHuoquView(APIView):
"""
@@ -3791,8 +3387,7 @@ class DashouDingdanHuoquView(APIView):
return {}
-# apps/dingdan/views.py
-from shangpin.models import ShangpinLeixing # 假设这是你的商品类型模型
+
class ShangpinLeixingHuoquView(APIView):
@@ -3834,114 +3429,6 @@ class ShangpinLeixingHuoquView(APIView):
})
-# apps/dingdan/views.py
-from django.db import transaction
-from django.utils import timezone
-from django.db.models import F
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework_simplejwt.authentication import JWTAuthentication
-import logging
-
-from .models import Dingdan, Liaotian
-
-from utils.goeasy_service import GoEasyService
-
-logger = logging.getLogger(__name__)
-
-
-from peizhi.models import Club
-
-logger = logging.getLogger(__name__)
-
-
-# apps/dingdan/views.py
-from django.db import transaction
-from django.utils import timezone
-from django.db.models import F
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework_simplejwt.authentication import JWTAuthentication
-import logging
-import requests
-import json
-
-from .models import Dingdan, DingdanPingtai, DingdanShangjia
-
-
-logger = logging.getLogger(__name__)
-
-from django.db import transaction
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from django.utils import timezone
-from django.db.models import F
-from django.conf import settings
-import json
-import requests
-from datetime import datetime
-
-
-from peizhi.models import Club
-
-logger = logging.getLogger(__name__)
-
-
-# dingdan/views.py
-import logging
-import requests
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status, permissions
-from .models import Dingdan, DingdanPingtai, DingdanShangjia
-from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia
-from peizhi.models import ClubConfig
-from utils.chat_utils import _send_group_message, _subscribe_users_to_group
-
-logger = logging.getLogger('dingdan')
-
-# dingdan/views.py
-# dingdan/views.py
-import logging
-import requests
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status, permissions
-from .models import Dingdan, DingdanPingtai, DingdanShangjia
-from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia
-from peizhi.models import ClubConfig
-
-logger = logging.getLogger('dingdan')
-
-
-# dingdan/views.py
-
-import logging
-import requests
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import permissions
-from .models import Dingdan, DingdanPingtai, DingdanShangjia
-from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia
-from peizhi.models import ClubConfig
-
-logger = logging.getLogger('dingdan')
-
-import logging
-import requests
-import time
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import permissions
-
-from .models import Dingdan, DingdanPingtai, DingdanShangjia
-from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia
-from peizhi.models import ClubConfig
-
-logger = logging.getLogger('dingdan')
# ----------------- 基础配置获取 -----------------
@@ -4252,11 +3739,6 @@ class DaiLiQunLiaoXiaoXiView(APIView):
#通过跨平台字段判断是否是跨平台
-
-
-from peizhi.models import ClubConfig, ShangjiaLianjie
-logger = logging.getLogger('dingdan')
-
# ---------- 基础配置 ----------
def get_self_goeasy_appkey():
try:
@@ -4377,8 +3859,6 @@ class ShangjiaLianjieLiuYanZhuanFaView(APIView):
return Response({'code': 200, 'msg': '我方发送成功,对方发送失败'})
-from peizhi.models import ClubConfig
-from utils.chat_utils import establish_order_chat # 【新增】导入聊天工具
class QiangdanView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
@@ -4398,13 +3878,11 @@ class QiangdanView(APIView):
# ==================== 【新增】罚单检查 ====================
- from utils.fadan_utils import check_fadan_qiangdan_eligible
fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(request.user.yonghuid,1)
if not fadan_ok:
return Response({'code': 400, 'msg': fadan_msg})
# =========================================================
# ========== 标签快速校验(提前拒绝,避免锁资源) ==========
- from dengji.utils import check_dashou_biaoqian_required
biaoqian_ok, biaoqian_msg = check_dashou_biaoqian_required(dingdan_id, request.user.yonghuid)
if not biaoqian_ok:
return Response({'code': 400, 'msg': biaoqian_msg})
@@ -4479,7 +3957,6 @@ class QiangdanView(APIView):
# 在 QiangdanView.post 里,_execute_qiangdan 之后添加:
- from dengji.services import record_dashou_biaoxian
# 记录抢单表现(不影响主流程)
# 如果订单表里有 bankuai 信息,可以用 order.bankuai 代替 None
record_dashou_biaoxian(
@@ -4764,8 +4241,6 @@ class DashouDingdanHuoquView1(APIView):
}, status=status.HTTP_200_OK)
except Exception as e:
- import logging
- logger = logging.getLogger(__name__)
logger.error(f"打手订单获取失败: {str(e)}", exc_info=True)
return Response({
@@ -4775,21 +4250,6 @@ class DashouDingdanHuoquView1(APIView):
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-#
-# views.py - 打手COS凭证接口(完整替换)
-import json
-import time
-from tencentcloud.common import credential
-from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
-from tencentcloud.sts.v20180813 import sts_client, models
-from django.conf import settings
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from rest_framework import status
-import logging
-
-logger = logging.getLogger(__name__)
class DashouCOSZhengshuView(APIView):
"""打手获取COS上传凭证接口(调试版本)"""
@@ -4811,7 +4271,6 @@ class DashouCOSZhengshuView(APIView):
# 3. 业务验证逻辑(根据用途分别验证)
if yongtu == 'chufa':
try:
- from .models import Chufajilu
chufa_record = Chufajilu.objects.filter(
dingdan_id=dingdan_id,
dashouid=request.user.yonghuid,
@@ -4835,7 +4294,6 @@ class DashouCOSZhengshuView(APIView):
else:
# 默认为订单相关,需要验证订单且打手必须接单
try:
- from .models import Dingdan
dingdan = Dingdan.objects.get(
dingdan_id=dingdan_id,
jiedan_dashou_id=request.user.yonghuid,
@@ -4971,8 +4429,6 @@ class ShangjiaCOSZhengshuView(APIView):
# 3. 🔴【修改】验证订单权限 - 通过商家订单扩展表验证
try:
- from .models import Dingdan, DingdanShangjia
-
# 获取订单
dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
@@ -4994,7 +4450,6 @@ class ShangjiaCOSZhengshuView(APIView):
# 🔴【新增】检查是否已存在处罚申请(通过Chufajilu模型)
# 注意:这里假设Chufajilu模型存在,并且有shangjia_id字段
try:
- from .models import Chufajilu
existing_chufa = Chufajilu.objects.filter(
dingdan_id=dingdan_id,
qingqiuid=shangjia_id,
@@ -5213,7 +4668,6 @@ class DashouTijiaoView(APIView):
dashou_profile.zhuangtai = 1 # 改为数字1(根据你之前说的)
dashou_profile.save()
- from dengji.services import record_dashou_biaoxian
# ========== 新增:记录打手提交时间 ==========
record_dashou_biaoxian(
dingdan_id=dingdan_id,
@@ -5235,8 +4689,6 @@ class DashouTijiaoView(APIView):
}, status=status.HTTP_200_OK)
except Exception as e:
- import logging
- logger = logging.getLogger(__name__)
logger.error(f"打手提交订单失败: {str(e)}", exc_info=True)
return Response({
@@ -5376,8 +4828,6 @@ class DashouDingdanXiangqingView(APIView):
}, status=status.HTTP_200_OK)
except Exception as e:
- import logging
- logger = logging.getLogger(__name__)
logger.error(f"获取打手订单详情失败: {str(e)}", exc_info=True)
return Response({
@@ -5387,16 +4837,6 @@ class DashouDingdanXiangqingView(APIView):
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-# dingdan/views.py
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-import logging
-
-
-logger = logging.getLogger(__name__)
class AdGetDingdanXiangQing(APIView):
@@ -5454,7 +4894,6 @@ class AdGetDingdanXiangQing(APIView):
# 6. 查询订单主表
try:
- from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia, Dashoutupian
# 查询订单主表 - 使用select_related预加载关联表
dingdan_obj = Dingdan.objects.select_related(
@@ -5554,8 +4993,6 @@ class AdGetDingdanXiangQing(APIView):
# 🔥 新增:查询退款记录表中的商家退款理由
try:
# 导入退款记录模型
- from dingdan.models import Tuikuanjilu
-
# 查询该订单的退款记录(一个订单可能有多次退款申请,我们取最新的)
tuikuan_record = Tuikuanjilu.objects.filter(
dingdan_id=dingdan_id
@@ -5664,7 +5101,6 @@ class AdGengHuanDaShou(APIView):
# 6. 查询订单主表
try:
- from dingdan.models import Dingdan
dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
@@ -5721,7 +5157,6 @@ class AdGengHuanDaShou(APIView):
# 查询会员购买记录
try:
- from shangpin.models import Huiyuangoumai
huiyuan_goumai = Huiyuangoumai.objects.get(
huiyuan_id=huiyuan_id,
yonghu_id=new_dashou_id # 注意:yonghu_id存储的是用户ID(yonghuid)
@@ -5821,12 +5256,6 @@ class AdGengHuanDaShou(APIView):
)
-import logging
-
-# 获取日志器
-logger = logging.getLogger(__name__)
-
-
class AdQiangZhiJieDan(APIView):
"""
后台强制结单接口
@@ -5891,7 +5320,6 @@ class AdQiangZhiJieDan(APIView):
# 6. 查询订单主表
try:
- from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia
dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
@@ -5998,7 +5426,6 @@ class AdQiangZhiJieDan(APIView):
# 12. 更新平台收益流水(仅在平台发单时)
if dingdan_obj.fadan_pingtai == 1:
try:
- from dingdan.utils import update_platform_profit # 导入函数
update_platform_profit(dingdan_id)
except Exception as e:
# 平台收益更新失败不影响主流程,但记录日志
@@ -6084,7 +5511,6 @@ class AdJuJueTuiKuan(APIView):
# 6. 查询订单主表
try:
- from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu
dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
@@ -6293,7 +5719,6 @@ class AdTongYiTuiKuanShangJia(APIView):
# 7. 查询订单主表
try:
- from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu
dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
@@ -6431,29 +5856,6 @@ class AdTongYiTuiKuanShangJia(APIView):
)
-# dingdan/views.py
-import logging
-import time
-import random
-import hashlib
-import requests
-import xml.etree.ElementTree as ET
-from decimal import Decimal
-from django.db import transaction
-from django.conf import settings
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-
-from yonghu.models import UserMain, UserDashou, UserBoss
-from dingdan.models import Dingdan, DingdanPingtai, Tuikuanjilu
-from peizhi.models import Szjilu
-
-logger = logging.getLogger(__name__)
-
-
class AdTongYiTuiKuanPingTai(APIView):
"""
后台同意退款接口(平台发单)
@@ -6603,8 +6005,6 @@ class AdTongYiTuiKuanPingTai(APIView):
# 16. 更新收支记录表(id=1)
try:
- #更新支出记录
- from dingdan.utils import update_daily_payout
update_daily_payout(jine)
@@ -6792,9 +6192,6 @@ class DashouHuoquLeixingView(APIView):
try:
logger.info(f"打手获取商品类型请求 - 用户ID: {request.user.yonghuid}")
- # 🔴 导入商品类型模型
- from shangpin.models import ShangpinLeixing
-
# 🔴 查询所有正常状态的商品类型
# 条件:shenhezhuangtai=1(正常状态)
# 排序:先按 paixu 降序(数值大的在前),再按 id 升序
@@ -6840,11 +6237,7 @@ class DashouHuoquLeixingView(APIView):
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-logger = logging.getLogger(__name__)
-from peizhi.models import Club ,OrderTypeMapping
-
-logger = logging.getLogger(__name__)
class PartnerSyncOrderView(APIView):
@@ -6987,10 +6380,6 @@ class PartnerSyncOrderView(APIView):
logger.info(f"跨平台订单同步成功: partner_order_id={partner_order_id}, local_id={new_id}")
-
- #🟢【7. 新增】支付成功后的广播通知(异步,不阻塞主流程)
- from dingdan.tongzhi_tasks import dingdan_guangbo
-
# 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用)
order_info = {
@@ -7016,7 +6405,6 @@ class PartnerSyncOrderView(APIView):
def _get_game_type_name(self, leixing_id):
try:
- from shangpin.models import ShangpinLeixing
gt = ShangpinLeixing.objects.filter(id=leixing_id).first()
return gt.jieshao if gt else '游戏订单'
except Exception as e:
@@ -7024,9 +6412,7 @@ class PartnerSyncOrderView(APIView):
return '游戏订单'
-from dingdan.utils import update_daily_dispatch_stat
-logger = logging.getLogger(__name__)
class PartnerCheckClaimView(APIView):
@@ -7598,8 +6984,7 @@ class DashouXiugaiView(APIView):
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-from django.core.exceptions import ObjectDoesNotExist
-from dingdan.tongzhi_tasks import dingdan_guangbo
+
class ZxsjghdsView(APIView):
"""
@@ -7708,7 +7093,6 @@ class ZxsjghdsView(APIView):
# 6.4 打手每日统计(退款行为)
if dashou_id:
try:
- from houtai.utils import update_dashou_daily_by_action
update_dashou_daily_by_action(
yonghuid=dashou_id,
amount=dashou_fencheng,
@@ -7721,7 +7105,6 @@ class ZxsjghdsView(APIView):
# 6.5 跨平台退款通知
if need_notify_partner:
try:
- from peizhi.models import ClubConfig, Club
my_club_config = ClubConfig.objects.filter(is_self=1).first()
if not my_club_config:
raise Exception("我方俱乐部配置不存在")
@@ -7748,7 +7131,6 @@ class ZxsjghdsView(APIView):
return Response({'code': 500, 'msg': f"对方平台通知失败: {resp_json.get('msg')}"})
# 更新跨平台扩展数据
- from dingdan.models import CrossPlatformOrderData
CrossPlatformOrderData.objects.update_or_create(
dingdan_id=dingdan_id,
defaults={
diff --git a/peizhi/__init__.py b/products/__init__.py
similarity index 100%
rename from peizhi/__init__.py
rename to products/__init__.py
diff --git a/peizhi/admin.py b/products/admin.py
similarity index 100%
rename from peizhi/admin.py
rename to products/admin.py
diff --git a/shangpin/apps.py b/products/apps.py
similarity index 100%
rename from shangpin/apps.py
rename to products/apps.py
diff --git a/shangpin/migrations/0001_initial.py b/products/migrations/0001_initial.py
similarity index 100%
rename from shangpin/migrations/0001_initial.py
rename to products/migrations/0001_initial.py
diff --git a/shangpin/migrations/0002_alter_huiyuangoumai_unique_together_and_more.py b/products/migrations/0002_alter_huiyuangoumai_unique_together_and_more.py
similarity index 100%
rename from shangpin/migrations/0002_alter_huiyuangoumai_unique_together_and_more.py
rename to products/migrations/0002_alter_huiyuangoumai_unique_together_and_more.py
diff --git a/shangpin/migrations/0003_shangpin_paixu_shangpinleixing_paixu_and_more.py b/products/migrations/0003_shangpin_paixu_shangpinleixing_paixu_and_more.py
similarity index 100%
rename from shangpin/migrations/0003_shangpin_paixu_shangpinleixing_paixu_and_more.py
rename to products/migrations/0003_shangpin_paixu_shangpinleixing_paixu_and_more.py
diff --git a/shangpin/migrations/0004_shangpin_shenhezhuangtai_and_more.py b/products/migrations/0004_shangpin_shenhezhuangtai_and_more.py
similarity index 100%
rename from shangpin/migrations/0004_shangpin_shenhezhuangtai_and_more.py
rename to products/migrations/0004_shangpin_shenhezhuangtai_and_more.py
diff --git a/shangpin/migrations/0005_alter_gsfenhong_dashouid.py b/products/migrations/0005_alter_gsfenhong_dashouid.py
similarity index 100%
rename from shangpin/migrations/0005_alter_gsfenhong_dashouid.py
rename to products/migrations/0005_alter_gsfenhong_dashouid.py
diff --git a/shangpin/migrations/0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong.py b/products/migrations/0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong.py
similarity index 100%
rename from shangpin/migrations/0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong.py
rename to products/migrations/0006_gsfenhong_huiyuan_id_huiyuan_zuzhangfc_duocifenhong.py
diff --git a/shangpin/migrations/0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more.py b/products/migrations/0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more.py
similarity index 100%
rename from shangpin/migrations/0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more.py
rename to products/migrations/0007_remove_duocifenhong_duoci_fenho_guanshi_b3de53_idx_and_more.py
diff --git a/shangpin/migrations/0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id.py b/products/migrations/0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id.py
similarity index 100%
rename from shangpin/migrations/0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id.py
rename to products/migrations/0008_gsfenhong_zuzhang_fenhong_gsfenhong_zuzhang_id.py
diff --git a/shangpin/migrations/0009_shangpin_ewai_dashou_fencheng_and_more.py b/products/migrations/0009_shangpin_ewai_dashou_fencheng_and_more.py
similarity index 100%
rename from shangpin/migrations/0009_shangpin_ewai_dashou_fencheng_and_more.py
rename to products/migrations/0009_shangpin_ewai_dashou_fencheng_and_more.py
diff --git a/shangpin/migrations/0010_shangpinleixing_is_cross_enabled.py b/products/migrations/0010_shangpinleixing_is_cross_enabled.py
similarity index 100%
rename from shangpin/migrations/0010_shangpinleixing_is_cross_enabled.py
rename to products/migrations/0010_shangpinleixing_is_cross_enabled.py
diff --git a/shangpin/migrations/0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more.py b/products/migrations/0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more.py
similarity index 100%
rename from shangpin/migrations/0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more.py
rename to products/migrations/0011_shangpin_dianpu_shangpin_dianpu_leixing_and_more.py
diff --git a/shangpin/migrations/0012_shangpinmeiritongji.py b/products/migrations/0012_shangpinmeiritongji.py
similarity index 100%
rename from shangpin/migrations/0012_shangpinmeiritongji.py
rename to products/migrations/0012_shangpinmeiritongji.py
diff --git a/shangpin/migrations/0013_shangpinleixing_bankuai.py b/products/migrations/0013_shangpinleixing_bankuai.py
similarity index 100%
rename from shangpin/migrations/0013_shangpinleixing_bankuai.py
rename to products/migrations/0013_shangpinleixing_bankuai.py
diff --git a/shangpin/migrations/0014_huiyuan_bankuai.py b/products/migrations/0014_huiyuan_bankuai.py
similarity index 100%
rename from shangpin/migrations/0014_huiyuan_bankuai.py
rename to products/migrations/0014_huiyuan_bankuai.py
diff --git a/shangpin/migrations/0015_gsfenhong_fenhong_leixing.py b/products/migrations/0015_gsfenhong_fenhong_leixing.py
similarity index 100%
rename from shangpin/migrations/0015_gsfenhong_fenhong_leixing.py
rename to products/migrations/0015_gsfenhong_fenhong_leixing.py
diff --git a/peizhi/migrations/__init__.py b/products/migrations/__init__.py
similarity index 100%
rename from peizhi/migrations/__init__.py
rename to products/migrations/__init__.py
diff --git a/shangpin/models.py b/products/models.py
similarity index 100%
rename from shangpin/models.py
rename to products/models.py
diff --git a/peizhi/tests.py b/products/tests.py
similarity index 100%
rename from peizhi/tests.py
rename to products/tests.py
diff --git a/shangpin/urls.py b/products/urls.py
similarity index 99%
rename from shangpin/urls.py
rename to products/urls.py
index 95f9c61..bfd630c 100644
--- a/shangpin/urls.py
+++ b/products/urls.py
@@ -65,7 +65,6 @@ urlpatterns = [
path('adhyjlcxhq',HuiyuanRecordListView.as_view(), name='管理员查询会员记录'),
-
path('zuzhangfenhong', ZuzhangFenhongView.as_view(), name='组长分红记录获取'),
path('czhqdy', CzhqdyView.as_view(), name='充值打手抵押查询'),
diff --git a/shangpin/utils.py b/products/utils.py
similarity index 99%
rename from shangpin/utils.py
rename to products/utils.py
index 4bc52ab..78df05e 100644
--- a/shangpin/utils.py
+++ b/products/utils.py
@@ -6,7 +6,7 @@ from decimal import Decimal
from django.db import transaction
from django.db.models import F
from .models import ShangpinMeiriTongji
-from shangpin.models import Shangpin
+from products.models import Shangpin
logger = logging.getLogger(__name__)
diff --git a/shangpin/views.py b/products/views.py
similarity index 75%
rename from shangpin/views.py
rename to products/views.py
index 60d3512..4ced5df 100644
--- a/shangpin/views.py
+++ b/products/views.py
@@ -1,18 +1,65 @@
-# shangpin/views.py
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.throttling import AnonRateThrottle
-from django.db import models
-from shangpin.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin
+# ==================== 标准库 ====================
+import os
+import json
+import time
+import random
+import string
+import uuid
+import hashlib
+import requests
import logging
-from rest_framework.permissions import AllowAny # 新增这行
+import decimal
+from datetime import timedelta
+from decimal import Decimal, InvalidOperation, ROUND_CEILING
+import defusedxml.ElementTree as ET
-# 配置日志
+# ==================== Django 内置 ====================
+from django.conf import settings
+from django.db import models, transaction, connection
+from django.db.models import Q, Count, Subquery, OuterRef
+from django.core.paginator import Paginator
+from django.shortcuts import render, get_object_or_404
+from django.http import HttpResponse
+from django.utils import timezone
+
+# ==================== DRF 框架 ====================
+from rest_framework.views import APIView, View
+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 utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
+from orders.utils import update_daily_income
+from backend.utils import (
+ update_guanshi_daily_by_action,
+ update_guanshi_xufei_daily,
+ update_zuzhang_daily_by_action
+)
+from users.fadan_fenhong_utils import process_fadan_fenhong
+
+# ==================== 各App模型 ====================
+from .models import (
+ ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan,
+ Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong
+)
+from users.models import (
+ UserMain, AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia
+)
+
+from orders.models import Lilubiao
+from config.models import Szjilu, Gonggao, Lunbo
+
+from orders.models import Fadan # 用于罚款类型
+
+import traceback
+
+# ==================== 日志实例 ====================
logger = logging.getLogger(__name__)
-
class ShangpinHuoquView(APIView):
"""
商品数据获取接口
@@ -119,215 +166,6 @@ class ShangpinHuoquView(APIView):
-'''class ShangpinHuoquView(APIView):
- """
- 商品数据获取接口
- 一次性返回商品类型、商品专区、商品列表
- """
-
- # 应用节流(限制频率)
- throttle_classes = [AnonRateThrottle]
- permission_classes = [AllowAny]
-
- def post(self, request):
- """
- 处理POST请求,返回所有商品相关数据
- """
- try:
- logger.info("收到商品数据获取请求")
-
- # 🔴【修改】1. 查询商品类型(只查询正常状态的类型,并排序)
- # 正常运营:只显示 shenhezhuangtai=1 的商品类型
- leixing_queryset = ShangpinLeixing.objects.filter(
- shenhezhuangtai=2 # 🔴 只取正常状态
- ).order_by('-paixu', 'id') # 🔴 先按paixu降序,再按id升序
-
- # 🔴【注意】审核期间使用下面这行代码(先注释掉)
- """leixing_queryset = ShangpinLeixing.objects.filter(
- shenhezhuangtai=2 # 🔴 审核期间:只取审核专用状态
- ).order_by('-paixu', 'id')"""
-
- shangpinleixing_data = []
- for leixing in leixing_queryset:
- shangpinleixing_data.append({
- "id": leixing.id,
- "jieshao": leixing.jieshao,
- "tupian_url": leixing.tupian_url,
- "paixu": leixing.paixu, # 🔴 新增字段
- "shenhezhuangtai": leixing.shenhezhuangtai # 🔴 新增字段
- })
-
- # 🔴【修改】2. 查询商品专区(只查询可见类型下的专区,并排序)
- # 先获取可见的类型ID列表
- visible_leixing_ids = [leixing['id'] for leixing in shangpinleixing_data]
-
- if visible_leixing_ids:
- zhuanqu_queryset = ShangpinZhuanqu.objects.filter(
- leixing_id__in=visible_leixing_ids
- ).order_by('-paixu', 'id') # 🔴 先按paixu降序,再按id升序
- else:
- zhuanqu_queryset = ShangpinZhuanqu.objects.none()
-
- shangpinzhuanqu_data = []
- for zhuanqu in zhuanqu_queryset:
- shangpinzhuanqu_data.append({
- "id": zhuanqu.id,
- "mingzi": zhuanqu.mingzi,
- "leixing_id": zhuanqu.leixing_id,
- "paixu": zhuanqu.paixu # 🔴 新增字段
- })
-
- # 🔴【修改】3. 查询商品列表(只查询可见专区下的商品,并排序)
- # 先获取可见的专区ID列表
- visible_zhuanqu_ids = [zhuanqu['id'] for zhuanqu in shangpinzhuanqu_data]
-
- if visible_zhuanqu_ids:
- # 注意:这里使用values()只获取指定字段,提高查询效率
- shangpin_queryset = Shangpin.objects.filter(
- zhuanqu_id__in=visible_zhuanqu_ids
- ).values(
- 'id', 'biaoqian', 'jiage', 'leixing_id',
- 'zhuanqu_id', 'tupian_url', 'duiwai_xiaoliang', 'paixu' # 🔴 新增paixu字段
- ).order_by('-paixu', 'id') # 🔴 先按paixu降序,再按id升序
-
- # 将QuerySet转换为列表
- shangpinliebiao_data = list(shangpin_queryset)
- else:
- shangpinliebiao_data = []
-
- # 4. 构建返回数据
- response_data = {
- "shangpinleixing": shangpinleixing_data,
- "shangpinzhuanqu": shangpinzhuanqu_data,
- "shangpinliebiao": shangpinliebiao_data
- }
-
- logger.info(f"返回数据:类型{len(shangpinleixing_data)}个,"
- f"专区{len(shangpinzhuanqu_data)}个,"
- f"商品{len(shangpinliebiao_data)}个")
-
- # 5. 返回响应
- return Response(response_data, status=status.HTTP_200_OK)
-
- except Exception as e:
- # 记录异常
- logger.error(f"获取商品数据时发生错误:{str(e)}", exc_info=True)
-
- # 返回错误响应
- error_data = {
- "error": "服务器内部错误",
- "detail": "获取商品数据失败,请稍后重试"
- }
- return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)'''
-
-
-'''class ShangpinHuoquView(APIView):
- """
- 商品数据获取接口
- 一次性返回商品类型、商品专区、商品列表
- """
-
- # 应用节流(限制频率)
- throttle_classes = [AnonRateThrottle]
- permission_classes = [AllowAny]
-
- def post(self, request):
- """
- 处理POST请求,返回所有商品相关数据
-
- 请求: POST /api/shangpin/shangpinhuoqu/
- 返回: {
- "shangpinleixing": [
- {
- "id": 1,
- "jieshao": "和平精英",
- "tupian_url": "leixing/peace.jpg"
- }
- ],
- "shangpinzhuanqu": [
- {
- "id": 1,
- "mingzi": "地铁逃生",
- "leixing_id": 1
- }
- ],
- "shangpinliebiao": [
- {
- "id": 1,
- "biaoqian": "战神段位陪玩",
- "jiage": 50.00,
- "tupian_url": "shangpin/1.jpg",
- "leixing_id": 1,
- "zhuanqu_id": 1,
- "duiwai_xiaoliang": 100
- }
- ]
- }
- """
- try:
- logger.info("收到商品数据获取请求")
-
- # 1. 查询商品类型(所有字段)
- leixing_queryset = ShangpinLeixing.objects.all()
- shangpinleixing_data = []
-
- for leixing in leixing_queryset:
- shangpinleixing_data.append({
- "id": leixing.id,
- "jieshao": leixing.jieshao,
- "tupian_url": leixing.tupian_url
- })
-
- # 2. 查询商品专区(所有字段)
- zhuanqu_queryset = ShangpinZhuanqu.objects.all()
- shangpinzhuanqu_data = []
-
- for zhuanqu in zhuanqu_queryset:
- shangpinzhuanqu_data.append({
- "id": zhuanqu.id,
- "mingzi": zhuanqu.mingzi,
- "leixing_id": zhuanqu.leixing_id
- })
-
- # 3. 查询商品列表(只查询需要的字段,减少数据传输量)
- # 注意:这里使用values()只获取指定字段,提高查询效率
- shangpin_queryset = Shangpin.objects.all().values(
- 'id', 'biaoqian', 'jiage', 'leixing_id',
- 'zhuanqu_id', 'tupian_url', 'duiwai_xiaoliang'
- )
-
- # 将QuerySet转换为列表
- shangpinliebiao_data = list(shangpin_queryset)
-
- # 4. 构建返回数据
- response_data = {
- "shangpinleixing": shangpinleixing_data,
- "shangpinzhuanqu": shangpinzhuanqu_data,
- "shangpinliebiao": shangpinliebiao_data
- }
-
- logger.info(f"返回数据:类型{len(shangpinleixing_data)}个,"
- f"专区{len(shangpinzhuanqu_data)}个,"
- f"商品{len(shangpinliebiao_data)}个")
-
- # 5. 返回响应
- return Response(response_data, status=status.HTTP_200_OK)
-
- except Exception as e:
- # 记录异常
- logger.error(f"获取商品数据时发生错误:{str(e)}")
-
- # 返回错误响应
- error_data = {
- "error": "服务器内部错误",
- "detail": "获取商品数据失败,请稍后重试"
- }
- return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)'''
-
-
-
-
-
class ShangpinXiangqingHuoquView(APIView):
"""
商品详情接口 - 极简版
@@ -386,16 +224,7 @@ class ShangpinXiangqingHuoquView(APIView):
-# views.py
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from rest_framework import status
-from shangpin.models import Huiyuan
-from yonghu.models import UserDashou
-import logging
-logger = logging.getLogger(__name__)
class DashouHuiyuanList(APIView):
"""
@@ -460,23 +289,6 @@ class DashouHuiyuanList(APIView):
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-# views.py
-import random
-import string
-import time
-import hashlib
-import defusedxml.ElementTree as ET
-import requests
-from django.conf import settings
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from rest_framework import status
-from shangpin.models import Czjilu
-from yonghu.models import UserDashou
-import logging
-
-logger = logging.getLogger(__name__)
class YajinGoumai(APIView):
@@ -726,17 +538,6 @@ class YajinGoumai(APIView):
return ip if ip else '127.0.0.1'
-# views.py
-from django.views import View
-from django.http import HttpResponse
-from yonghu.models import UserDashou, UserMain
-from shangpin.models import Czjilu
-import defusedxml.ElementTree as ET
-import hashlib
-from django.conf import settings
-import logging
-
-logger = logging.getLogger(__name__)
@@ -808,7 +609,6 @@ class YajinHuitiao(View):
dashou = UserDashou.objects.get(user__yonghuid=order.yonghuid)
# 增加押金(使用Decimal确保精度)
- from decimal import Decimal
dashou.yajin += Decimal(str(order_amount_yuan))
dashou.save(update_fields=['yajin'])
@@ -816,11 +616,6 @@ class YajinHuitiao(View):
# 8b. 押金分红(仅微信确认到账后;费率 Lilubiao fadanpingtai=12)
try:
- from decimal import Decimal
- from django.db import transaction
- from dingdan.models import Lilubiao
- from shangpin.models import Gsfenhong
- from yonghu.models import UserGuanshi
guanshi_id = dashou.yaoqingren
if not guanshi_id:
@@ -860,7 +655,6 @@ class YajinHuitiao(View):
f"打手{order.yonghuid}, 金额{fenhong_jine}元"
)
try:
- from houtai.utils import update_guanshi_daily_by_action
update_guanshi_daily_by_action(
yonghuid=guanshi_id,
action=4,
@@ -885,16 +679,10 @@ class YajinHuitiao(View):
# 更新收支记录表
try:
- from peizhi.models import Szjilu
- from decimal import Decimal
-
-
-
# 获取订单金额(确保是Decimal类型)
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
# 查询或创建ID=1的收支记录
- from dingdan.utils import update_daily_income
update_daily_income(jine_decimal)
szjilu, created = Szjilu.objects.get_or_create(
@@ -1407,13 +1195,8 @@ class JifenHuitiao(View):
# 更新收支记录表
try:
- from peizhi.models import Szjilu
- from decimal import Decimal
-
# 获取订单金额(确保是Decimal类型)
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
-
- from dingdan.utils import update_daily_income
update_daily_income(jine_decimal)
# 查询或创建ID=1的收支记录
@@ -1675,24 +1458,6 @@ class JifenLunxun(APIView):
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-# views.py
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from rest_framework import status
-from shangpin.models import Huiyuan, Czjilu
-from yonghu.models import UserDashou
-import random
-import string
-import time
-import hashlib
-import defusedxml.ElementTree as ET
-import requests
-from django.conf import settings
-import logging
-
-logger = logging.getLogger(__name__)
-
class HuiyuanGoumai(APIView):
"""
@@ -1945,27 +1710,6 @@ class HuiyuanGoumai(APIView):
-
-
-
-
-
-
-
-# views.py
-
-from django.views import View
-from django.http import HttpResponse
-from yonghu.models import UserDashou, UserMain, UserGuanshi,UserZuzhang
-from shangpin.models import Czjilu, Huiyuan, Huiyuangoumai, Gsfenhong,DuociFenhong
-from peizhi.models import Szjilu
-from datetime import timedelta
-import logging
-
-logger = logging.getLogger(__name__)
-
-
-
class HuiyuanHuitiao(View):
"""
会员支付回调接口
@@ -2029,7 +1773,6 @@ class HuiyuanHuitiao(View):
try:
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
- from dingdan.utils import update_daily_income
update_daily_income(jine_decimal)
szjilu, _ = Szjilu.objects.get_or_create(
@@ -2243,7 +1986,6 @@ class HuiyuanHuitiao(View):
# ========== 管事每日统计(新购) ==========
if guanshi_fenhong > 0 and cishu == 1:
try:
- from houtai.utils import update_guanshi_daily_by_action
update_guanshi_daily_by_action(
yonghuid=guanshi_id,
action=2,
@@ -2256,7 +1998,6 @@ class HuiyuanHuitiao(View):
# ========== 管事每日统计(续费) ==========
if guanshi_fenhong > 0 and cishu > 1:
try:
- from houtai.utils import update_guanshi_xufei_daily
update_guanshi_xufei_daily(
yonghuid=guanshi_id,
xufei_jine=guanshi_fenhong
@@ -2268,7 +2009,6 @@ class HuiyuanHuitiao(View):
# ========== 组长每日统计(分佣收入) ==========
if zuzhang_fenhong > 0 and zuzhang_id:
try:
- from houtai.utils import update_zuzhang_daily_by_action
update_zuzhang_daily_by_action(
yonghuid=zuzhang_id,
action=2,
@@ -2378,471 +2118,6 @@ class HuiyuanHuitiao(View):
-
-
-
-# views.py
-
-'''from django.views import View
-from django.http import HttpResponse
-from yonghu.models import UserDashou, UserMain, UserGuanshi
-from shangpin.models import Czjilu, Huiyuan, Huiyuangoumai, Gsfenhong
-import defusedxml.ElementTree as ET
-import hashlib
-from django.conf import settings
-from django.utils import timezone
-from datetime import timedelta
-import logging
-
-logger = logging.getLogger(__name__)
-
-
-class HuiyuanHuitiao(View):
- """
- 会员支付回调接口
- 路径:/shangpin/huiyuanhuitiao
- 注意:这个接口不需要JWT认证,由微信服务器调用
- """
-
- def post(self, request):
- try:
- # 1. 解析微信回调XML数据
- request_body = request.body
- logger.info(f"收到会员支付回调,原始数据: {request_body.decode('utf-8')}")
-
- root = ET.fromstring(request_body)
-
- # 获取关键字段
- return_code = root.find('return_code').text
- result_code = root.find('result_code').text
- out_trade_no = root.find('out_trade_no').text # 订单ID
- transaction_id = root.find('transaction_id').text # 微信交易号
- total_fee = int(root.find('total_fee').text) # 支付金额(分)
-
- logger.info(f"会员回调数据: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分")
-
- # 2. 验证基本返回状态
- if return_code != 'SUCCESS' or result_code != 'SUCCESS':
- logger.warning(f"会员支付回调状态异常: {return_code}/{result_code}")
- return self.wechat_response('FAIL', '支付失败')
-
- # 3. 验证签名(完整版)
- if not self.verify_signature(request_body):
- logger.error(f"会员签名验证失败: {out_trade_no}")
- return self.wechat_response('FAIL', '签名验证失败')
-
- # 4. 查询订单
- try:
- order = Czjilu.objects.get(dingdan_id=out_trade_no)
- logger.info(f"找到会员订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}")
- except Czjilu.DoesNotExist:
- logger.error(f"会员订单不存在: {out_trade_no}")
- return self.wechat_response('FAIL', '订单不存在')
-
- # 5. 检查订单类型是否为会员(1)
- if order.leixing != 1:
- logger.error(f"订单类型错误: {order.leixing}, 期望1(会员)")
- return self.wechat_response('FAIL', '订单类型错误')
-
- # 6. 检查订单状态是否为未支付(9)
- if order.zhuangtai != 9:
- logger.warning(f"会员订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
- return self.wechat_response('SUCCESS', 'OK')
-
- # 7. 验证金额
- wechat_amount_yuan = total_fee / 100 # 转换为元
- order_amount_yuan = float(order.jine) # 订单金额(元)
-
- # 允许1分钱的误差
- if abs(wechat_amount_yuan - order_amount_yuan) > 0.01:
- logger.error(f"会员金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
- return self.wechat_response('FAIL', '金额不一致')
-
- # 8. 查询会员信息
- try:
- huiyuan = Huiyuan.objects.get(huiyuan_id=order.huiyuan_id)
- except Huiyuan.DoesNotExist:
- logger.error(f"会员信息不存在: {order.huiyuan_id}")
- return self.wechat_response('FAIL', '会员信息不存在')
-
- # ===== 在这里添加收支记录更新代码 =====
- # 更新收支记录表
- try:
- from peizhi.models import Szjilu
- from decimal import Decimal
-
- # 获取订单金额(确保是Decimal类型)
- jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
-
- # 查询或创建ID=1的收支记录
- szjilu, created = Szjilu.objects.get_or_create(
- id=1,
- defaults={
- 'zongsy': Decimal('0.00'),
- 'zongls': Decimal('0.00'),
- 'zongzc': Decimal('0.00'),
- 'jrzc': Decimal('0.00'),
- 'jrls': Decimal('0.00')
- }
- )
-
- # 更新三个字段
- szjilu.zongsy += jine_decimal # 总收益
- szjilu.zongls += jine_decimal # 总流水
- szjilu.jrls += jine_decimal # 今日流水
-
- szjilu.save()
-
- logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元")
-
- except Exception as e:
- # 收支记录更新失败不影响主流程,只记录日志
- logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
- # ===== 添加结束 =====
-
-
- # 9. 更新订单状态为已支付(3)
- order.zhuangtai = 3
- order.save(update_fields=['zhuangtai'])
- logger.info(f"会员订单状态更新成功: {order.dingdan_id} -> 3")
-
- # 10. 更新会员表的购买次数
- huiyuan.goumai_cishu += 1
- huiyuan.save(update_fields=['goumai_cishu'])
-
- # 11. 处理会员购买记录
- #self.process_huiyuan_goumai(order.yonghuid, order.huiyuan_id, huiyuan)
-
- goumai_record, is_new, is_first_buy = self.process_huiyuan_goumai(
- order.yonghuid, order.huiyuan_id, huiyuan
- )
-
- # 12. 如果是首次购买任何会员,加10积分
- if is_first_buy:
- self.add_first_buy_jifen(order.yonghuid)
-
- # 12. 处理管事分红(只有在第一次购买时才分红)
- self.process_guanshi_fenhong(order, huiyuan)
-
- # 13. 返回成功响应给微信
- logger.info(f"会员支付回调处理完成: {out_trade_no}")
- return self.wechat_response('SUCCESS', 'OK')
-
- except ET.ParseError as e:
- logger.error(f"会员XML解析失败: {str(e)}")
- return self.wechat_response('FAIL', '数据格式错误')
- except Exception as e:
- logger.error(f"会员支付回调处理异常: {str(e)}", exc_info=True)
- return self.wechat_response('FAIL', '处理异常')
-
-
- def process_huiyuan_goumai(self, yonghuid, huiyuan_id, huiyuan):
- """处理会员购买记录(优化版)"""
- try:
- # 1. 先尝试获取现有记录(如果存在就是续费)
- existing_record = Huiyuangoumai.objects.filter(
- yonghu_id=yonghuid,
- huiyuan_id=huiyuan_id
- ).first()
-
- is_first_buy_any = False # 标记是否首次购买任何会员
-
- if existing_record:
- # 已存在记录,续费
- existing_record.daoqi_time += timedelta(days=30)
- existing_record.huiyuan_zhuangtai = 1
- existing_record.save()
- logger.info(f"会员续费: 用户{yonghuid}, 会员{huiyuan_id}")
- return existing_record, False, False # (记录, 是否新建, 是否首次买任何)
- else:
- # 2. 检查是否是首次购买任何会员(关键查询!)
- # 查询该用户是否买过其他会员
- other_huiyuan_count = Huiyuangoumai.objects.filter(
- yonghu_id=yonghuid
- ).exclude(huiyuan_id=huiyuan_id).count() # 排除本次会员
-
- if other_huiyuan_count == 0:
- # 这是该用户第一次买任何会员!
- is_first_buy_any = True
- logger.info(f"用户{yonghuid}首次购买任何会员,将获得10积分")
-
- # 3. 创建新记录
- goumai_record = Huiyuangoumai.objects.create(
- yonghu_id=yonghuid,
- huiyuan_id=huiyuan_id,
- jieshao=huiyuan.jieshao,
- huiyuan_zhuangtai=1,
- daoqi_time=timezone.now() + timedelta(days=30)
- )
- logger.info(f"新购会员: 用户{yonghuid}, 会员{huiyuan_id}")
- return goumai_record, True, is_first_buy_any
-
- except Exception as e:
- logger.error(f"处理会员购买记录失败: {str(e)}", exc_info=True)
- # 如果是因为唯一约束冲突(数据库已拦截)
- if "unique" in str(e).lower() or "duplicate" in str(e).lower():
- logger.warning(f"检测到重复创建尝试,已由数据库拦截: 用户{yonghuid}, 会员{huiyuan_id}")
- # 重新查询并续费
- return self.process_huiyuan_goumai(yonghuid, huiyuan_id, huiyuan)
- raise
-
-
-
-
-
-
-
-
-
- def add_first_buy_jifen(self, yonghuid):
- """为首次购买会员的用户增加10积分"""
- try:
- # 找到对应的打手记录
- dashou = UserDashou.objects.get(user__yonghuid=yonghuid)
- # 增加10积分
- dashou.jifen += 10
- dashou.save(update_fields=['jifen'])
- logger.info(f"用户{yonghuid}首次购买会员,积分+10,当前积分: {dashou.jifen}")
-
- # 可选:记录积分变动历史
- # self.create_jifen_history(yonghuid, 10, "首次购买会员奖励")
-
- except UserDashou.DoesNotExist:
- logger.error(f"找不到打手记录,无法增加积分: {yonghuid}")
- except Exception as e:
- logger.error(f"增加积分失败: {str(e)}")
-
-
-
-
- #
- #
- #
- #
- #
-
- def process_huiyuan_goumai(self, yonghuid, huiyuan_id, huiyuan):
- """处理会员购买记录(完整版)"""
- try:
- # 查询是否已有购买记录
- goumai_record, created = Huiyuangoumai.objects.get_or_create(
- yonghu_id=yonghuid,
- huiyuan_id=huiyuan_id,
- defaults={
- 'jieshao': huiyuan.jieshao,
- 'huiyuan_zhuangtai': 1,
- 'daoqi_time': timezone.now() + timedelta(days=30)
- }
- )
-
- if not created:
- # 已存在记录,续费(在原来基础上加30天)
- goumai_record.daoqi_time += timedelta(days=30)
- goumai_record.huiyuan_zhuangtai = 1 # 确保状态为激活
- goumai_record.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai'])
- logger.info(f"会员续费: 用户{yonghuid}, 会员{huiyuan_id}, 新到期时间{goumai_record.daoqi_time}")
- else:
- logger.info(f"新购会员: 用户{yonghuid}, 会员{huiyuan_id}, 到期时间{goumai_record.daoqi_time}")
-
- except Exception as e:
- logger.error(f"处理会员购买记录失败: {str(e)}", exc_info=True)
- #
- #
- #
- #
- # 继续执行,不中断流程
-
-
- def process_guanshi_fenhong(self, order, huiyuan):
- """
- 处理管事分红(支持首次和二次分红,特殊用户首次可配置)
- """
- try:
- # 1. 获取打手信息
- dashou = UserDashou.objects.get(user__yonghuid=order.yonghuid)
- yaoqingren = dashou.yaoqingren
- if not yaoqingren:
- logger.info(f"打手{order.yonghuid}没有邀请人,无分红")
- return
-
- # 2. 获取管事信息
- try:
- guanshi = UserGuanshi.objects.get(user__yonghuid=yaoqingren)
- except UserGuanshi.DoesNotExist:
- logger.warning(f"管事信息不存在: {yaoqingren}")
- return
-
- # 3. 获取打手头像和昵称
- user_main = dashou.user
- avatar = user_main.avatar
- nicheng = dashou.nicheng or "未知打手"
-
- # 4. 判断是该打手第几次分红(通过查询分红记录表)
- existing_count = Gsfenhong.objects.filter(dashouid=order.yonghuid).count()
- is_first = (existing_count == 0) # 第一次分红
-
- # 5. 确定分红金额
- fenhong_amount = None
- if is_first:
- # 首次分红
- special_ids = getattr(settings, 'SPECIAL_GUANSHI_IDS', [])
- if yaoqingren in special_ids:
- # 特殊管事:从管事扩展表获取首次分红金额
- fenhong_amount = guanshi.fenhong_yici
- logger.info(f"管事{yaoqingren}为特殊用户,首次分红金额取配置值: {fenhong_amount}")
- else:
- # 普通管事:从会员配置表获取分红金额
- fenhong_amount = huiyuan.guanshifc
- logger.info(f"管事{yaoqingren}为普通用户,首次分红金额取会员表: {fenhong_amount}")
- else:
- # 第二次及以上分红(目前只支持第二次)
- if guanshi.fenghong_erci_enabled:
- fenhong_amount = guanshi.fenghong_erci
- logger.info(f"管事{yaoqingren}开启二次分红,分红金额: {fenhong_amount}")
- else:
- logger.info(f"管事{yaoqingren}未开启二次分红,本次不分红")
- return
-
- # 6. 如果分红金额 <= 0,则不分红
- if fenhong_amount <= 0:
- logger.info(f"分红金额 {fenhong_amount} <=0,不分红")
- return
-
- # 7. 创建分红记录(注意:dashouid 唯一约束已移除)
- Gsfenhong.objects.create(
- dingdan_id=order.dingdan_id,
- guanshi=yaoqingren,
- dashouid=order.yonghuid,
- shuoming=order.shuoming,
- fenhong=fenhong_amount,
- avatar=avatar,
- nicheng=nicheng
- )
-
- # 8. 更新管事扩展表
- guanshi.chongzhifenrun += fenhong_amount
- guanshi.yue += fenhong_amount
- guanshi.jinrichongzhi += 1
- guanshi.jinyuechongzhi += 1
- guanshi.save(update_fields=['chongzhifenrun', 'yue', 'jinrichongzhi', 'jinyuechongzhi'])
-
- logger.info(f"创建分红记录成功: 管事{yaoqingren}, 打手{order.yonghuid}, 分红{fenhong_amount}元")
-
- except Exception as e:
- logger.error(f"处理管事分红失败: {str(e)}", exc_info=True)
- # 继续执行,不中断主流程
-
-
-
-
- #不针对特殊用户,且不含二次分红的版本
- #
- #
- #
- #def process_guanshi_fenhong(self, order, huiyuan):
- """处理管事分红(完整版,只有第一次购买才分红)"""
- try:
- # 查询打手信息
- dashou = UserDashou.objects.get(user__yonghuid=order.yonghuid)
-
- # 检查是否已有分红记录(确保只有第一次购买才分红)
- if Gsfenhong.objects.filter(dashouid=order.yonghuid).exists():
- logger.info(f"打手{order.yonghuid}已有分红记录,不再分红")
- return
-
- # 获取管事ID(邀请人ID)
- yaoqingren = dashou.yaoqingren
- if not yaoqingren:
- logger.info(f"打手{order.yonghuid}没有邀请人,无分红")
- return
-
- # 查询管事信息
- try:
- guanshi = UserGuanshi.objects.get(user__yonghuid=yaoqingren)
- except UserGuanshi.DoesNotExist:
- logger.warning(f"管事信息不存在: {yaoqingren}")
- return
-
- # 获取打手头像和昵称
- user_main = dashou.user
- avatar = user_main.avatar
- nicheng = dashou.nicheng or "未知打手"
-
- # 创建分红记录
- fenhong_record = Gsfenhong.objects.create(
- dingdan_id=order.dingdan_id,
- guanshi=yaoqingren,
- dashouid=order.yonghuid,
- shuoming=order.shuoming,
- fenhong=huiyuan.guanshifc, # 管事分成金额
- avatar=avatar,
- nicheng=nicheng
- )
-
- # 更新管事扩展表数据
- guanshi.chongzhifenrun += huiyuan.guanshifc
- guanshi.yue += huiyuan.guanshifc
- guanshi.jinrichongzhi += 1
- guanshi.jinyuechongzhi += 1
- guanshi.save(update_fields=['chongzhifenrun', 'yue', 'jinrichongzhi', 'jinyuechongzhi'])
-
- logger.info(f"创建分红记录: 管事{yaoqingren}, 打手{order.yonghuid}, 分成{huiyuan.guanshifc}元")
-
- except Exception as e:
- logger.error(f"处理管事分红失败: {str(e)}", exc_info=True)
- #
- #
- #
- #
- # 继续执行,不中断主流程
-
- def verify_signature(self, xml_data):
- """验证微信支付签名(完整版)"""
- try:
- root = ET.fromstring(xml_data)
- sign = root.find('sign').text
-
- if not sign:
- logger.error("会员签名字段不存在")
- return False
-
- # 获取所有参数并排除sign
- params = {}
- for child in root:
- if child.tag != 'sign' and child.text is not None:
- params[child.tag] = child.text
-
- # 按照参数名ASCII码从小到大排序
- sorted_params = sorted(params.items(), key=lambda x: x[0])
-
- # 拼接字符串
- sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
- sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
-
- # 计算签名
- calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
-
- is_valid = sign == calculated_sign
- if not is_valid:
- logger.error(f"会员签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}")
-
- return is_valid
-
- except Exception as e:
- logger.error(f"会员验证签名异常: {str(e)}")
- return False
-
- def wechat_response(self, return_code, return_msg):
- """返回微信支付回调响应(完整版)"""
- xml_response = f"""
-
-
-"""
-
- logger.info(f"会员返回微信响应: {return_code} - {return_msg}")
- return HttpResponse(xml_response, content_type='application/xml')'''
-
-
# views.py
class HuiyuanShibai(APIView):
"""
@@ -3012,24 +2287,6 @@ class HuiyuanLunxun(APIView):
# views.py in shangpin app
-import time
-import random
-import string
-import hashlib
-import defusedxml.ElementTree as ET
-import requests
-from decimal import Decimal
-from django.conf import settings
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from yonghu.models import UserMain, UserShangjia
-from shangpin.models import Czjilu
-import logging
-
-logger = logging.getLogger(__name__)
-
class ShangjiaChongzhi(APIView):
"""
@@ -3283,9 +2540,6 @@ class ShangjiaChongzhi(APIView):
-from django.db import transaction
-
-logger = logging.getLogger(__name__)
class ShangjiaHuitiao(View):
@@ -3324,7 +2578,6 @@ class ShangjiaHuitiao(View):
# 4. 查询订单
try:
- from decimal import Decimal
order = Czjilu.objects.get(dingdan_id=out_trade_no)
logger.info(f"找到商家订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}")
except Czjilu.DoesNotExist:
@@ -3349,13 +2602,9 @@ class ShangjiaHuitiao(View):
# ===== 在这里添加收支记录更新代码 =====
# 更新收支记录表
try:
- from peizhi.models import Szjilu
- from decimal import Decimal
-
# 获取订单金额(确保是Decimal类型)
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
- from dingdan.utils import update_daily_income
update_daily_income(jine_decimal)
# 查询或创建ID=1的收支记录
@@ -3614,13 +2863,6 @@ class ShangjiaChenggong(APIView):
# 管事分红记录查询API
-from django.db.models import Q
-from decimal import Decimal
-import logging
-
-logger = logging.getLogger(__name__)
-
-
class GuanshiFenhongListAPI(APIView):
"""
管事分红记录查询接口
@@ -3652,8 +2894,6 @@ class GuanshiFenhongListAPI(APIView):
query = Q(guanshi=yonghuid)
# 先查询总数(使用分页查询优化)
- from django.db.models import Count
- from .models import Gsfenhong
# 查询总记录数
zong_tiaoshu = Gsfenhong.objects.filter(query).count()
@@ -3701,23 +2941,6 @@ class GuanshiFenhongListAPI(APIView):
# views.py - 商品模块视图
-
-from django.shortcuts import render
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework import status
-from django.db.models import Q
-import logging
-
-# 导入模型
-from yonghu.models import UserMain, AdminProfile
-from shangpin.models import ShangpinLeixing, ShangpinZhuanqu, Huiyuan
-
-# 日志记录器
-logger = logging.getLogger(__name__)
-
-
class AdGetAllDataView(APIView):
"""
获取商品类型、商品专区、会员数据的接口
@@ -3876,21 +3099,6 @@ class AdGetAllDataView(APIView):
# views.py - shangpin模块
-from django.db import transaction
-from rest_framework.views import APIView
-from rest_framework import permissions, status
-from rest_framework.response import Response
-from django.shortcuts import get_object_or_404
-from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
-import os
-import uuid
-
-# 导入模型和工具
-from .models import ShangpinLeixing, Huiyuan, ShangpinZhuanqu, Shangpin
-from yonghu.models import UserMain, AdminProfile
-from utils.oss_utils import validate_image, upload_to_oss
-
-
class ShangpinLeixingFabuView(APIView):
"""
商品类型发布接口
@@ -4086,7 +3294,6 @@ class ShangpinLeixingFabuView(APIView):
except Exception as e:
# 捕获所有异常,返回500错误
- import traceback
traceback.print_exc()
return Response({
'code': 500,
@@ -4195,7 +3402,6 @@ class ShangpinZhuanquFabuView(APIView):
})
except Exception as e:
- import traceback
traceback.print_exc()
return Response({
'code': 500,
@@ -4500,7 +3706,6 @@ class ShangpinFabuView(APIView):
except Exception as e:
# 捕获所有异常,返回500错误
- import traceback
traceback.print_exc()
return Response({
'code': 500,
@@ -4643,7 +3848,6 @@ class GuizeTupianShangchuanView(APIView):
except Exception as e:
# 捕获所有异常,返回500错误
- import traceback
traceback.print_exc()
return Response({
'code': 500,
@@ -4751,7 +3955,6 @@ class AdHuiyuanHqView(APIView):
except Exception as e:
# 捕获所有异常,返回500错误
- import traceback
traceback.print_exc()
return Response({
'code': 500,
@@ -4923,7 +4126,6 @@ class HuiyuanXiugaiView(APIView):
except Exception as e:
# 捕获所有异常,返回500错误
- import traceback
traceback.print_exc()
return Response({
'code': 500,
@@ -5104,7 +4306,6 @@ class AdHuiyuanFbView(APIView):
except Exception as e:
# 捕获所有异常,返回500错误
- import traceback
traceback.print_exc()
return Response({
'code': 500,
@@ -5112,20 +4313,6 @@ class AdHuiyuanFbView(APIView):
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-from django.shortcuts import get_object_or_404
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework import status
-from django.db.models import Q
-
-# 导入模型
-from yonghu.models import UserMain, AdminProfile
-from .models import ShangpinLeixing, ShangpinZhuanqu, Huiyuan, Shangpin
-from peizhi.models import Gonggao, Lunbo
-
-
class AdShangpinHuQu(APIView):
"""
后台商品管理页面数据获取接口
@@ -5242,13 +4429,6 @@ class AdShangpinHuQu(APIView):
# shangpin/views.py
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework import status
-from yonghu.models import UserMain, AdminProfile
-from peizhi.models import Gonggao
-
class AdGonggaoXiuGai(APIView):
"""
@@ -5440,16 +4620,6 @@ class AdZhuanquXiuGai(APIView):
-from rest_framework.parsers import MultiPartParser, FormParser
-from django.db import transaction
-from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
-from peizhi.models import Lunbo
-import os
-import uuid
-import random
-import string
-
-
class AdLunboXiuGai(APIView):
"""
后台编辑轮播图接口
@@ -5610,249 +4780,6 @@ class AdLunboXiuGai(APIView):
)
-'''class AdLeixingXiuGai(APIView):
- """
- 后台编辑商品类型接口 - 优化版
- 支持:只改图片、只改字段、两者都改
- 拒绝:两者都不改
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = (MultiPartParser, FormParser, JSONParser) # 添加JSONParser支持
-
- def post(self, request):
- try:
- # 1. 权限验证(保持不变)
- qianzhanghao = request.data.get('zhanghao', '').strip()
- if not qianzhanghao:
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- dangqianyonghu = request.user
- if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if dangqianyonghu.user_type != 'admin':
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- guanliyuan = dangqianyonghu.admin_profile
- except AdminProfile.DoesNotExist:
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 获取商品类型ID
- leixing_id = request.data.get('leixing_id')
- if not leixing_id:
- return Response({'code': 400, 'message': '商品类型ID不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 查询商品类型
- try:
- leixing_obj = ShangpinLeixing.objects.get(id=leixing_id)
- except ShangpinLeixing.DoesNotExist:
- return Response({'code': 400, 'message': '商品类型不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 获取字段并初始化变更标记
- has_changed = False
- jieshao = request.data.get('jieshao', '').strip()
- yaoqiuleixing = request.data.get('yaoqiuleixing')
- huiyuan_id = request.data.get('huiyuan_id', '').strip()
- yongjin = request.data.get('yongjin', '').strip()
- tupian_url = request.data.get('tupian_url', '').strip() # 原有图片URL
-
- # 5. 检查非图片字段是否有变更
- if jieshao and jieshao != leixing_obj.jieshao:
- leixing_obj.jieshao = jieshao
- has_changed = True
-
- if yaoqiuleixing and int(yaoqiuleixing) != leixing_obj.yaoqiuleixing:
- leixing_obj.yaoqiuleixing = int(yaoqiuleixing)
- has_changed = True
-
- # 根据类型清空另一个字段
- if leixing_obj.yaoqiuleixing == 1:
- leixing_obj.yongjin = None
- if huiyuan_id:
- leixing_obj.huiyuan_id = huiyuan_id
- # 验证会员存在
- try:
- Huiyuan.objects.get(huiyuan_id=huiyuan_id)
- except Huiyuan.DoesNotExist:
- return Response({'code': 400, 'msg': '会员ID不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
- else: # 类型2
- leixing_obj.huiyuan_id = None
- if yongjin:
- try:
- leixing_obj.yongjin = float(yongjin)
- if leixing_obj.yongjin <= 0:
- return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
- except ValueError:
- return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # ===== 在现有代码的第5步后面添加 =====
-
- # 5.5 检查抢单要求类型不变时,会员ID或佣金是否有变更
- if yaoqiuleixing and int(yaoqiuleixing) == leixing_obj.yaoqiuleixing:
- # 类型没有变化,但需要检查对应字段的变化
-
- # 情况1:会员抢单 (yaoqiuleixing == 1)
- if leixing_obj.yaoqiuleixing == 1:
- # 检查会员ID是否变更
- if huiyuan_id and huiyuan_id != leixing_obj.huiyuan_id:
- # 验证会员存在
- try:
- Huiyuan.objects.get(huiyuan_id=huiyuan_id)
- except Huiyuan.DoesNotExist:
- return Response({'code': 400, 'msg': '会员ID不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- leixing_obj.huiyuan_id = huiyuan_id
- has_changed = True
- #print(f"会员ID变更: {leixing_obj.huiyuan_id} -> {huiyuan_id}")
-
- # 情况2:佣金抢单 (yaoqiuleixing == 2)
- elif leixing_obj.yaoqiuleixing == 2:
- # 检查佣金是否变更
- if yongjin:
- try:
- new_yongjin = float(yongjin)
- if new_yongjin <= 0:
- return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 判断是否有变化(考虑None的情况)
- current_yongjin = leixing_obj.yongjin
- if (current_yongjin is None and new_yongjin != 0) or \
- (current_yongjin is not None and abs(float(current_yongjin) - new_yongjin) > 0.001):
- leixing_obj.yongjin = new_yongjin
- has_changed = True
- #print(f"佣金变更: {current_yongjin} -> {new_yongjin}")
-
- except ValueError:
- return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # ===== 添加结束 =====
-
- # 6. 处理图片上传(如果有新图片)
- tupian_file = request.FILES.get('file')
- if tupian_file:
- # 验证图片
- is_valid, error_msg = validate_image(tupian_file)
- if not is_valid:
- return Response({'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 删除旧图片(如果存在)
- if leixing_obj.tupian_url:
- delete_from_oss(leixing_obj.tupian_url)
-
- # 生成文件名和路径
- file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg'
- oss_folder = 'a_long'
- leixing_folder = 'leixing'
-
- # 使用原文件名或生成新文件名
- if leixing_obj.tupian_url:
- original_filename = leixing_obj.tupian_url.split('/')[-1]
- else:
- original_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}"
-
- oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{original_filename}"
-
- # 上传到OSS
- new_tupian_url = upload_to_oss(tupian_file, oss_file_path)
- if not new_tupian_url:
- return Response({'code': 500, 'message': '图片上传失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- leixing_obj.tupian_url = oss_file_path # 存相对路径
- has_changed = True
- elif tupian_url and tupian_url != leixing_obj.tupian_url:
- # 前端传了新URL(可能是从其他地方选择的)
- leixing_obj.tupian_url = tupian_url
- has_changed = True
-
- # 7. 验证是否至少有变更
- if not has_changed:
- return Response({
- 'code': 400,
- 'message': '没有检测到任何更改',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 8. 保存并返回
- leixing_obj.save()
-
-
-
- # 🔴 ===========【新增:同步更新关联商品】===========
- try:
- from django.db.models import Q
-
- # 1. 准备商品更新字典
- shangpin_update_fields = {}
-
- # 始终同步抢单要求类型
- shangpin_update_fields['yaoqiuleixing'] = leixing_obj.yaoqiuleixing
-
- # 根据抢单类型,同步对应字段并清空另一个
- if leixing_obj.yaoqiuleixing == 1: # 会员抢单
- if leixing_obj.huiyuan_id: # 仅当会员ID非空时更新
- shangpin_update_fields['huiyuan_id'] = leixing_obj.huiyuan_id
- # 清空佣金字段
- shangpin_update_fields['yongjin'] = None
- elif leixing_obj.yaoqiuleixing == 2: # 佣金抢单
- if leixing_obj.yongjin: # 仅当佣金非空时更新
- shangpin_update_fields['yongjin'] = leixing_obj.yongjin
- # 清空会员字段
- shangpin_update_fields['huiyuan_id'] = None
-
- # 2. 执行批量更新(确保只有实际有变化的字段才会执行)
- if shangpin_update_fields:
- # 查询所有属于该类型的商品(使用filter而非all以提高效率)
- affected_count = Shangpin.objects.filter(
- leixing_id=leixing_obj.id
- ).update(**shangpin_update_fields)
-
- # 可选:记录日志以便追踪
- # logger.info(f"商品类型{leixing_obj.id}修改成功,已同步更新{affected_count}个关联商品")
-
- except Exception as sync_error:
- # 商品同步失败不影响商品类型本身的修改,但记录错误
- # 可根据需求决定是否抛出异常回滚整个事务
- # 当前设计:记录错误但允许商品类型修改成功
- logger.error(f"修改十五")
-
- # 可在此添加更详细的错误日志记录
- # 🔴 ===========【新增结束】===========
-
- response_data = {
- 'id': leixing_obj.id,
- 'jieshao': leixing_obj.jieshao,
- 'tupian_url': leixing_obj.tupian_url,
- 'yaoqiuleixing': leixing_obj.yaoqiuleixing,
- 'huiyuan_id': leixing_obj.huiyuan_id,
- 'yongjin': str(leixing_obj.yongjin) if leixing_obj.yongjin else None
- }
-
- return Response({
- 'code': 0,
- 'message': '商品类型修改成功',
- 'data': response_data
- })
-
- except Exception as e:
- print(f"编辑商品类型异常: {e}")
- return Response({'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR)'''
-
-
-
-
@@ -6038,8 +4965,6 @@ class AdLeixingXiuGai(APIView):
# 🔴 ===========【新增:同步更新关联商品】===========
try:
- from django.db.models import Q
-
# 1. 准备商品更新字典
shangpin_update_fields = {}
@@ -6210,437 +5135,9 @@ class AdShangpinNeiRongHuoQu(APIView):
)
-from rest_framework.parsers import MultiPartParser, FormParser
-from django.db import transaction
-'''class AdShangpinXiuGai(APIView):
- """
- 后台更改商品数据接口 - 修复版
- 修复:1. 图片替换逻辑 2. 对外销量字段处理
- """
- permission_classes = [IsAuthenticated]
- parser_classes = (MultiPartParser, FormParser, JSONParser)
-
- def post(self, request):
- try:
- # 1. 权限验证
- qianzhanghao = request.data.get('zhanghao', '').strip()
- shangpin_id = request.data.get('shangpin_id')
-
- if not qianzhanghao or not shangpin_id:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证身份
- dangqianyonghu = request.user
-
- if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- if dangqianyonghu.user_type != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- try:
- guanliyuan = dangqianyonghu.admin_profile
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 2. 判断操作类型:删除还是修改
- action = request.data.get('action', 'update').strip().lower()
-
- if action == 'delete':
- return self._delete_shangpin(shangpin_id)
- else:
- return self._update_shangpin(request, shangpin_id)
-
- except Exception as e:
- logger.error(f"更改商品数据异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- def _delete_shangpin(self, shangpin_id):
- """删除商品逻辑"""
- try:
- # 查询商品
- try:
- shangpin_obj = Shangpin.objects.get(id=shangpin_id)
- except Shangpin.DoesNotExist:
- return Response(
- {'code': 404, 'message': '商品不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 获取图片URL(如果有)
- tupian_url = shangpin_obj.tupian_url
- guizetupian_url = shangpin_obj.guize_tupian
-
- # 异步或同步删除OSS图片(互不影响)
- if tupian_url:
- try:
- delete_from_oss(tupian_url)
- except Exception as e:
- logger.warning(f"删除商品图片失败: {tupian_url}, 错误: {e}")
-
- if guizetupian_url:
- try:
- delete_from_oss(guizetupian_url)
- except Exception as e:
- logger.warning(f"删除规则图片失败: {guizetupian_url}, 错误: {e}")
-
- # 删除数据库记录
- shangpin_obj.delete()
-
- return Response({
- 'code': 0,
- 'message': '删除成功',
- 'data': {
- 'shangpin_id': shangpin_id,
- 'deleted': True
- }
- })
-
- except Exception as e:
- logger.error(f"删除商品异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '删除失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- def _update_shangpin(self, request, shangpin_id):
- """修改商品逻辑 - 修复:确保图片正确替换"""
- try:
- # 查询商品
- try:
- shangpin_obj = Shangpin.objects.get(id=shangpin_id)
- except Shangpin.DoesNotExist:
- return Response(
- {'code': 404, 'message': '商品不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- has_changes = False
-
- # 3. 处理图片上传(如果有新商品图片)
- tupian_file = request.FILES.get('tupian_file')
- if tupian_file:
- # 验证图片
- is_valid, error_msg = validate_image(tupian_file)
- if not is_valid:
- return Response(
- {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 获取原图片URL
- old_tupian_url = shangpin_obj.tupian_url
-
- # 确定OSS路径
- if old_tupian_url:
- # 有原图:使用原路径替换
- oss_file_path = old_tupian_url
-
- # 删除原图片
- try:
- delete_from_oss(old_tupian_url)
- except Exception as e:
- logger.warning(f"删除原商品图片失败: {old_tupian_url}, 错误: {e}")
- # 继续上传新图片
- else:
- # 没有原图:创建新路径
- oss_folder = 'a_long'
- shangpin_folder = 'shangpin'
- file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg'
- filename = f"{shangpin_id}{file_extension}"
- oss_file_path = f"{oss_folder}/{shangpin_folder}/{filename}"
-
- # 上传到OSS
- new_tupian_url = upload_to_oss(tupian_file, oss_file_path)
-
- if not new_tupian_url:
- return Response(
- {'code': 500, 'message': '图片上传失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- # 更新数据库中的图片URL
- shangpin_obj.tupian_url = oss_file_path # 存相对路径
- has_changes = True
-
- # 4. 处理其他字段(只更新有变化的字段)
- field_mapping = {
- 'biaoti': 'biaoqian',
- 'jieshao': 'jieshao',
- 'jiage': 'jiage',
- 'kucun': 'kucun',
- 'duiwaixiaoliang': 'duiwai_xiaoliang', # 关键:对外销量字段
- 'xiadanxuzhi': 'xiadan_xuzhi',
- 'yajin': 'yajin',
- }
-
- for front_field, db_field in field_mapping.items():
- if front_field in request.data:
- new_value = request.data.get(front_field)
-
- # 空字符串处理
- if new_value == '':
- new_value = None
-
- if new_value is not None:
- # 获取当前值
- current_value = getattr(shangpin_obj, db_field)
-
- # 类型转换
- try:
- if db_field in ['jiage', 'yajin']:
- # 价格和押金
- if new_value:
- new_value = Decimal(str(new_value))
- else:
- new_value = None
- elif db_field in ['kucun', 'duiwai_xiaoliang']:
- # 库存和对外销量
- if new_value:
- new_value = int(float(new_value))
- else:
- new_value = 0
- except (ValueError, TypeError, InvalidOperation) as e:
- logger.warning(f"字段{front_field}转换失败: {new_value}, 错误: {e}")
- continue
-
- # 检查是否有变化(处理None值)
- if current_value != new_value:
- setattr(shangpin_obj, db_field, new_value)
- has_changes = True
-
- # 5. 如果没有检测到任何更改
- if not has_changes:
- return Response({
- 'code': 400,
- 'message': '没有检测到任何更改',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 6. 保存更改
- shangpin_obj.save()
-
- # 7. 返回更新后的数据
- response_data = {
- 'tupian_url': shangpin_obj.tupian_url or '',
- 'biaoti': shangpin_obj.biaoqian or '',
- 'jieshao': shangpin_obj.jieshao or '',
- 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00',
- 'kucun': shangpin_obj.kucun,
- 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang, # 返回对外销量
- 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '',
- 'yajin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None,
- 'genggaishijian': shangpin_obj.update_time.strftime(
- '%Y-%m-%d %H:%M:%S') if shangpin_obj.update_time else '',
- }
-
- return Response({
- 'code': 0,
- 'message': '更新成功',
- 'data': response_data
- })
-
- except Exception as e:
- logger.error(f"更新商品异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '更新失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-import os
-from decimal import Decimal, InvalidOperation
-
-
-class AdShangpinGuizeXiuGai(APIView):
- """
- 后台规则图片更换接口 - 修复版
- 修复:确保替换时使用正确的路径
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = (MultiPartParser, FormParser, JSONParser)
-
- def post(self, request):
- try:
- # 1. 权限验证
- qianzhanghao = request.data.get('zhanghao', '').strip()
- shangpin_id = request.data.get('shangpin_id')
- action = request.data.get('action', 'update').strip().lower()
-
- if not qianzhanghao or not shangpin_id:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证身份
- dangqianyonghu = request.user
-
- if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- if dangqianyonghu.user_type != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- try:
- guanliyuan = dangqianyonghu.admin_profile
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 2. 查询商品
- try:
- shangpin_obj = Shangpin.objects.get(id=shangpin_id)
- except Shangpin.DoesNotExist:
- return Response(
- {'code': 404, 'message': '商品不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 3. 处理不同操作
- if action == 'delete':
- return self._delete_guize_image(shangpin_obj)
- elif action == 'update':
- return self._update_guize_image(request, shangpin_obj)
- else:
- return Response(
- {'code': 400, 'message': '操作类型无效', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- except Exception as e:
- logger.error(f"规则图片操作异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- def _delete_guize_image(self, shangpin_obj):
- """删除规则图片"""
- guizetupian_url = shangpin_obj.guize_tupian
-
- if guizetupian_url:
- # 从OSS删除
- try:
- delete_success = delete_from_oss(guizetupian_url)
- if not delete_success:
- logger.warning(f"OSS删除失败,但继续删除数据库记录: {guizetupian_url}")
- except Exception as e:
- logger.warning(f"删除规则图片异常: {e}")
-
- # 清除数据库记录
- shangpin_obj.guize_tupian = ''
- shangpin_obj.save()
-
- return Response({
- 'code': 0,
- 'message': '删除成功',
- 'data': {
- 'guizetupian_url': ''
- }
- })
-
- def _update_guize_image(self, request, shangpin_obj):
- """更新规则图片 - 修复:确保正确替换"""
- guize_file = request.FILES.get('guize_file')
-
- if not guize_file:
- return Response(
- {'code': 400, 'message': '请上传规则图片', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证图片
- is_valid, error_msg = validate_image(guize_file)
- if not is_valid:
- return Response(
- {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 处理路径
- oss_folder = 'a_long'
- spgz_folder = 'Spgz'
- file_extension = os.path.splitext(guize_file.name)[1].lower() or '.jpg'
-
- # 获取原规则图片URL
- old_guizetupian_url = shangpin_obj.guize_tupian
-
- if old_guizetupian_url:
- # 有原图:使用原路径替换
- oss_file_path = old_guizetupian_url
-
- # 删除原图片
- try:
- delete_from_oss(old_guizetupian_url)
- except Exception as e:
- logger.warning(f"删除原规则图片失败: {old_guizetupian_url}, 错误: {e}")
- # 继续上传新图片
- else:
- # 没有原图:创建新路径
- filename = f"{shangpin_obj.id}{file_extension}"
- oss_file_path = f"{oss_folder}/{spgz_folder}/{filename}"
-
- # 上传到OSS(替换或新建)
- new_guizetupian_url = upload_to_oss(guize_file, oss_file_path)
-
- if not new_guizetupian_url:
- return Response(
- {'code': 500, 'message': '图片上传失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- # 更新数据库(存相对路径)
- shangpin_obj.guize_tupian = oss_file_path # 存相对路径,如 "a_long/Spgz/123.jpg"
- shangpin_obj.save()
-
- return Response({
- 'code': 0,
- 'message': '规则图片更新成功',
- 'data': {
- 'guizetupian_url': oss_file_path # 返回相对路径
- }
- })'''
-
-import os
-import time
-import random
-from decimal import Decimal, InvalidOperation
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
-from rest_framework.permissions import IsAuthenticated
-import logging
-
-logger = logging.getLogger(__name__)
class AdShangpinXiuGai(APIView):
@@ -7067,28 +5564,7 @@ class AdShangpinGuizeXiuGai(APIView):
-
-
-
-
-
-
-
-
# views.py
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from rest_framework import status
-from django.utils import timezone
-from django.db.models import Q, Count, Subquery, OuterRef
-from django.db import connection
-from django.core.paginator import Paginator
-import json
-
-from .models import Huiyuan
-from yonghu.models import UserMain, AdminProfile
-
class HuiyuanTypeListView(APIView):
"""
获取会员类型列表接口
@@ -7333,7 +5809,6 @@ class HuiyuanRecordListView(APIView):
except Exception as e:
print(f"获取会员记录失败: {str(e)}")
- import traceback
traceback.print_exc()
return Response({
'code': 500,
@@ -7344,14 +5819,6 @@ class HuiyuanRecordListView(APIView):
-
-
-
-
-
-
-import decimal
-
class ZuzhangFenhongView(APIView):
"""
组长分红记录接口
@@ -7505,520 +5972,10 @@ class ZuzhangFenhongView(APIView):
-
-
-from dingdan.models import Lilubiao
-
-# 常量定义(可根据需要调整)
-INTEGRAL_PRICE = decimal.Decimal('5.00') # 积分购买固定价格(元)
-
-
-'''class CzhqdyView(APIView):
- """
- 获取可用抵扣身份接口
- 路径:/shangpin/czhqdy
- 方法:POST
- 权限:JWT认证
- 请求参数:
- leixing: int # 1=会员,2=押金,3=积分
- huiyuan_id: str # 当 leixing=1 时必传,会员ID
- yajin_jine: decimal # 当 leixing=2 时必传,押金金额(元)
- 返回:
- {
- "code": 200,
- "msg": "success",
- "data": [
- {
- "id": 1, # 身份ID:1打手 2管事 3组长
- "jieshao": "打手佣金抵扣",
- "xuyao": "22.22" # 所需抵扣金额(保留两位小数)
- },
- ...
- ]
- }
- """
- permission_classes = [permissions.IsAuthenticated]
-
- # 身份ID与对应扩展表模型、余额字段、费率查询条件的映射
- IDENTITY_CONFIG = [
- {
- 'id': 1,
- 'name': '打手佣金抵扣',
- 'profile_attr': 'dashou_profile', # UserMain 的反向 related_name
- 'balance_field': 'yue',
- 'rate_key': '5',
- 'model': UserDashou,
- },
- {
- 'id': 2,
- 'name': '管事分红抵扣',
- 'profile_attr': 'guanshi_profile',
- 'balance_field': 'yue',
- 'rate_key': '6',
- 'model': UserGuanshi,
- },
- {
- 'id': 3,
- 'name': '组长分红抵扣',
- 'profile_attr': 'zuzhang_profile',
- 'balance_field': 'ketixian_jine',
- 'rate_key': '8',
- 'model': UserZuzhang,
- },
- ]
-
- def post(self, request):
- # 获取参数
- leixing = request.data.get('leixing')
- huiyuan_id = request.data.get('huiyuan_id')
- yajin_jine = request.data.get('yajin_jine')
-
- # 参数校验
- if leixing not in [1, 2, 3,4]:
- return Response({'code': 400, 'msg': 'leixing 必须为 '}, status=status.HTTP_400_BAD_REQUEST)
-
- if leixing == 1 and not huiyuan_id:
- return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST)
-
- if leixing == 2:
- if not yajin_jine:
- return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST)
- try:
- yajin_jine = decimal.Decimal(str(yajin_jine))
- if yajin_jine <= 0:
- raise ValueError
- except:
- return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取当前用户
- user = request.user
-
- # 获取目标价格(需支付的原价)
- target_price = None
- if leixing == 1:
- # 会员
- try:
- huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id)
- target_price = huiyuan.jiage
- except Huiyuan.DoesNotExist:
- return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND)
- elif leixing == 2:
- # 押金
- target_price = yajin_jine
-
-
-
-
- elif leixing == 4:
- from dingdan.models import Fadan # 罚单模型
- # 其他 UserDashou、UserGuanshi、UserZuzhang、Lilubiao、Czjilu 等已有
- # 罚款缴纳
- fadan_id = request.data.get('fadan_id')
- if not fadan_id:
- return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST)
- try:
- fadan = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid)
- target_price = fadan.fakuanjine
- except Fadan.DoesNotExist:
- return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND)
-
-
-
-
- else: # leixing == 3
- # 积分(固定价格)
- target_price = INTEGRAL_PRICE
-
- # 获取所有费率(打手、管事、组长)
- rates = {}
- for key in ['5', '6', '8']:
- try:
- rate_obj = Lilubiao.objects.get(fadanpingtai=key)
- rates[key] = rate_obj.lilu
- except Lilubiao.DoesNotExist:
- # 如果某个费率不存在,则跳过该身份(后续不会返回)
- rates[key] = None
-
- # 构建可用身份列表
- available_list = []
-
- for ident in self.IDENTITY_CONFIG:
- # 检查用户是否有该扩展表
- profile = getattr(user, ident['profile_attr'], None)
- if not profile:
- continue
-
- # 获取余额
- balance = getattr(profile, ident['balance_field'], decimal.Decimal('0.00'))
-
- # 获取费率
- rate = rates.get(ident['rate_key'])
- if rate is None:
- continue # 费率未配置,跳过该身份
-
- # 计算所需抵扣金额: target_price / (1 - rate) ,向上取整到分
- # 注意 rate 是小数,如 0.08 表示 8%
- try:
- # 防止除零
- if rate >= 1:
- continue
- required = target_price / (decimal.Decimal('1.0') - rate)
- # 向上取整到分(保留两位小数,最后一位进位)
- # 使用 quantize 向上舍入: ROUND_CEILING
- required = required.quantize(decimal.Decimal('0.01'), rounding=decimal.ROUND_CEILING)
- except Exception as e:
- continue # 计算异常,跳过
-
- # 余额是否足够
- if balance >= required:
- available_list.append({
- 'id': ident['id'],
- 'jieshao': ident['name'],
- 'xuyao': str(required),
- })
-
- return Response({
- 'code': 200,
- 'msg': 'success',
- 'data': available_list
- })'''
-
-
-
-
-
-
-'''from decimal import Decimal, ROUND_CEILING
-from dingdan.models import Fadan
# 常量定义(可根据需要调整)
INTEGRAL_PRICE = Decimal('5.00') # 积分购买固定价格(元)
-INTEGRAL_INCREMENT = 5 # 每次积分补充增加5分
-MAX_INTEGRAL = 10 # 最大积分限制
-
-# 身份映射配置
-IDENTITY_CONFIG = {
- 1: {
- 'name': '打手佣金',
- 'model': UserDashou,
- 'balance_field': 'yue',
- 'status_field': 'zhanghaozhuangtai', # 1正常
- 'rate_key': '5',
- },
- 2: {
- 'name': '管事分红',
- 'model': UserGuanshi,
- 'balance_field': 'yue',
- 'status_field': 'zhuangtai', # 1正常
- 'rate_key': '6',
- },
- 3: {
- 'name': '组长分红',
- 'model': UserZuzhang,
- 'balance_field': 'ketixian_jine',
- 'status_field': 'zhuangtai', # 1正常
- 'rate_key': '8',
- },
-}
-from dingdan.models import Fadan
-from shangpin.models import Huiyuan
-
-
-class DsqrgmdhView(APIView):
- """
- 余额抵扣支付接口
- 路径:/shangpin/dsqrgmdh
- 方法:POST
- 权限:JWT认证
- 请求参数:
- leixing: int # 购买类型:1会员 2押金 3积分
- shenfen_id: int # 抵扣身份:1打手 2管事 3组长
- huiyuan_id: str # 当 leixing=1 时必传,会员ID
- yajin_jine: decimal # 当 leixing=2 时必传,押金金额(元)
- 返回:
- {
- "code": 200,
- "msg": "success",
- "data": {
- # 根据购买类型返回对应字段
- "yajin": 1000, # 押金更新后(leixing=2)
- "jifen": 10, # 积分更新后(leixing=3)
- "huiyuan": { ... } # 会员购买记录(leixing=1)
- # 以及其他可能需要的数据
- }
- }
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- # 获取参数
- leixing = request.data.get('leixing')
- shenfen_id = request.data.get('shenfen_id')
- huiyuan_id = request.data.get('huiyuan_id')
- yajin_jine = request.data.get('yajin_jine')
-
- # ----- 参数基本校验 -----
- if leixing not in [1, 2, 3,4]:
- return Response({'code': 400, 'msg': 'leixing 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST)
- if shenfen_id not in [1, 2, 3]:
- return Response({'code': 400, 'msg': 'shenfen_id 必须为 1/2/3'}, status=status.HTTP_400_BAD_REQUEST)
-
- if leixing == 1 and not huiyuan_id:
- return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST)
- if leixing == 2:
- if not yajin_jine:
- return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST)
- try:
- yajin_jine = Decimal(str(yajin_jine))
- if yajin_jine <= 0:
- raise ValueError
- if yajin_jine > Decimal('10000'):
- return Response({'code': 400, 'msg': '押金金额不能超过10000元'}, status=status.HTTP_400_BAD_REQUEST)
- except:
- return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST)
-
- # ----- 获取目标价格(需支付的原价) -----
- target_price = None
- if leixing == 1:
- try:
- huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id)
- target_price = huiyuan.jiage
- except Huiyuan.DoesNotExist:
- return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND)
- elif leixing == 2:
- target_price = yajin_jine
-
-
- elif leixing == 4:
- from dingdan.models import Fadan
- fadan_id = request.data.get('fadan_id')
- if not fadan_id:
- return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST)
- try:
- fadan = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid)
- target_price = fadan.fakuanjine
- except Fadan.DoesNotExist:
- return Response({'code': 404, 'msg': '罚单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
-
-
- else: # leixing == 3
- target_price = INTEGRAL_PRICE
-
- # ----- 获取对应身份费率 -----
- identity = IDENTITY_CONFIG.get(shenfen_id)
- try:
- rate_obj = Lilubiao.objects.get(fadanpingtai=identity['rate_key'])
- rate = rate_obj.lilu # Decimal
- if rate >= 1:
- return Response({'code': 400, 'msg': f"{identity['name']}费率异常"}, status=status.HTTP_400_BAD_REQUEST)
- except Lilubiao.DoesNotExist:
- return Response({'code': 400, 'msg': f"{identity['name']}费率未配置"}, status=status.HTTP_400_BAD_REQUEST)
-
- # ----- 计算所需抵扣金额(向上取整到分) -----
- try:
- required = target_price / (Decimal('1.0') - rate)
- required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING)
- except Exception as e:
- return Response({'code': 500, 'msg': f'金额计算错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # ----- 获取当前用户 -----
- user = request.user
- yonghuid = user.yonghuid
-
- # ----- 获取对应身份扩展表并验证状态和余额 -----
- try:
- profile = identity['model'].objects.get(user=user)
- except identity['model'].DoesNotExist:
- return Response({'code': 403, 'msg': f'您不是{identity["name"]}身份'})
-
- # 验证账号状态
- status_value = getattr(profile, identity['status_field'])
- if status_value != 1:
- return Response({'code': 403, 'msg': f'{identity["name"]}账号状态异常,无法使用'})
-
- # 验证余额是否足够
- balance = getattr(profile, identity['balance_field'])
- if balance < required:
- return Response({'code': 400, 'msg': f'{identity["name"]}余额不足,需要{required}元,当前{balance}元'}, status=status.HTTP_400_BAD_REQUEST)
-
- # ----- 开始事务处理 -----
- try:
- with transaction.atomic():
- # 锁定扩展表记录(防止并发扣除)
- locked_profile = identity['model'].objects.select_for_update().get(id=profile.id)
-
- # 再次验证余额(安全)
- current_balance = getattr(locked_profile, identity['balance_field'])
- if current_balance < required:
- raise ValueError(f"{identity['name']}余额不足,需要{required}元,当前{current_balance}元")
-
- # 扣除余额
- setattr(locked_profile, identity['balance_field'], current_balance - required)
- locked_profile.save(update_fields=[identity['balance_field']])
-
- # ----- 生成订单ID -----
- timestamp = str(int(time.time() * 1000))[-12:]
- rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
- dingdan_id = f"CZ{timestamp}{rand_str}"
-
- # 构建订单说明
- if leixing == 1:
- action_desc = f"购买会员 {huiyuan.jieshao}"
- elif leixing == 2:
- action_desc = f"充值押金 {yajin_jine}元"
-
- elif leixing ==4:
- action_desc = "罚款缴纳"
- else:
- action_desc = "补充积分"
-
- shuoming = f"{identity['name']}抵扣 {action_desc},抵扣金额{required}元"
-
- # 创建充值记录
- czjilu = Czjilu.objects.create(
- dingdan_id=dingdan_id,
- huiyuan_id=huiyuan_id if leixing == 1 else None,
- zhuangtai=1, # 已完成
- yonghuid=yonghuid,
- jine=required, # 实际扣除的金额
- leixing=leixing,
- shuoming=shuoming,
- )
-
- # ----- 根据购买类型执行具体业务逻辑 -----
- response_data = {}
-
- if leixing == 1: # 会员
- # 处理会员购买/续费
- huiyuan_record, is_first_buy = self._process_member_purchase(yonghuid, huiyuan_id, huiyuan)
- response_data['huiyuan'] = {
- 'huiyuanid': huiyuan_id,
- 'huiyuanming': huiyuan.jieshao,
- 'daoqi': huiyuan_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(huiyuan_record, 'daoqi_time') else '',
- }
-
- # 如果首次购买任何会员,给打手加10积分
- if is_first_buy:
- try:
- dashou = UserDashou.objects.get(user=user)
- dashou.jifen += 10
- dashou.save(update_fields=['jifen'])
- response_data['jifen'] = dashou.jifen
- except UserDashou.DoesNotExist:
- pass
-
- elif leixing == 2: # 押金
- # 更新打手押金
- try:
- dashou = UserDashou.objects.select_for_update().get(user=user)
- dashou.yajin += yajin_jine # 注意:增加的是原价,不是抵扣金额
- dashou.save(update_fields=['yajin'])
- response_data['yajin'] = dashou.yajin
- except UserDashou.DoesNotExist:
- return Response({'code': 500, 'msg': '打手信息不存在,无法充值押金'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
- elif leixing == 4: # 🔥 罚款:只更新罚单状态,不重复扣款
-
- fadan_id = request.data.get('fadan_id')
- if not fadan_id:
- return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST)
- try:
- fadan = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid)
- except Fadan.DoesNotExist:
- return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND)
-
- if fadan.zhuangtai not in [1, 3]:
- return Response({'code': 400, 'msg': '罚单状态不可缴纳'}, status=status.HTTP_400_BAD_REQUEST)
-
- fadan.zhuangtai = 2 # 已缴纳
- fadan.save(update_fields=['zhuangtai'])
- response_data['fadan_id'] = fadan.id
- response_data['fadan_status'] = fadan.zhuangtai
- from utils.fadan_utils import fadan_fenhong
- fadan_fenhong(fadan.id)
-
-
-
- else: # leixing == 3 积分
- # 更新打手积分
- try:
- dashou = UserDashou.objects.select_for_update().get(user=user)
- new_jifen = dashou.jifen + INTEGRAL_INCREMENT
- dashou.jifen = min(new_jifen, MAX_INTEGRAL) # 不超过10分
- dashou.save(update_fields=['jifen'])
- response_data['jifen'] = dashou.jifen
- except UserDashou.DoesNotExist:
- return Response({'code': 500, 'msg': '打手信息不存在,无法补充积分'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
- except Exception as e:
- import traceback
- traceback.print_exc()
- return Response({'code': 500, 'msg': f'支付处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # ----- 返回成功响应,附带更新后的数据 -----
- # 补充其他可能需要的数据(如更新后的余额等)
- # 可以查询最新的余额返回
- final_profile = identity['model'].objects.get(user=user)
- response_data['balance'] = str(getattr(final_profile, identity['balance_field']))
-
- return Response({
- 'code': 200,
- 'msg': 'success',
- 'data': response_data,
- })
-
- def _process_member_purchase(self, yonghuid, huiyuan_id, huiyuan_obj):
- """
- 处理会员购买/续费
- 返回 (Huiyuangoumai对象, is_first_buy_any)
- """
- is_first_buy_any = False
- try:
- # 检查是否已有该会员记录
- record = Huiyuangoumai.objects.filter(
- yonghu_id=yonghuid,
- huiyuan_id=huiyuan_id
- ).first()
-
- if record:
- # 续费
- record.daoqi_time += timedelta(days=30)
- record.huiyuan_zhuangtai = 1
- record.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai'])
- else:
- # 新购
- # 检查用户是否购买过任何会员(用于判断是否首次购买)
- other_count = Huiyuangoumai.objects.filter(yonghu_id=yonghuid).exclude(huiyuan_id=huiyuan_id).count()
- if other_count == 0:
- is_first_buy_any = True
-
- record = Huiyuangoumai.objects.create(
- yonghu_id=yonghuid,
- huiyuan_id=huiyuan_id,
- jieshao=huiyuan_obj.jieshao,
- huiyuan_zhuangtai=1,
- daoqi_time=timezone.now() + timedelta(days=30)
- )
- return record, is_first_buy_any
- except Exception as e:
- # 如果出现唯一性冲突(极低概率),重试一次
- if 'unique' in str(e).lower() or 'duplicate' in str(e).lower():
- # 直接查询已存在的记录
- record = Huiyuangoumai.objects.get(yonghu_id=yonghuid, huiyuan_id=huiyuan_id)
- return record, False
- raise'''
-
-
-
-
-from dingdan.models import Fadan # 用于罚款类型
class CzhqdyView(APIView):
"""
获取可用抵扣身份接口
@@ -8101,7 +6058,7 @@ class CzhqdyView(APIView):
if not yajin_jine:
return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST)
try:
- yajin_jine = decimal.Decimal(str(yajin_jine))
+ yajin_jine = Decimal(str(yajin_jine))
if yajin_jine <= 0:
raise ValueError
except:
@@ -8155,7 +6112,7 @@ class CzhqdyView(APIView):
continue
# 获取余额
- balance = getattr(profile, ident['balance_field'], decimal.Decimal('0.00'))
+ balance = getattr(profile, ident['balance_field'], Decimal('0.00'))
# 获取费率
rate = rates.get(ident['rate_key'])
@@ -8166,9 +6123,9 @@ class CzhqdyView(APIView):
try:
if rate >= 1:
continue # 防止除零及不合理费率
- required = target_price / (decimal.Decimal('1.0') - rate)
+ required = target_price / (Decimal('1.0') - rate)
# 向上取整到分(保留两位小数,最后一位进位)
- required = required.quantize(decimal.Decimal('0.01'), rounding=decimal.ROUND_CEILING)
+ required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING)
except Exception:
continue # 计算异常,跳过
@@ -8188,13 +6145,6 @@ class CzhqdyView(APIView):
-
-
-
-from decimal import Decimal, ROUND_CEILING
-# ========== 修改点1: 导入最新的分红处理方法 ==========
-from yonghu.fadan_fenhong_utils import process_fadan_fenhong
-
INTEGRAL_INCREMENT = 5
MAX_INTEGRAL = 10
diff --git a/shangdian/__init__.py b/rank/__init__.py
similarity index 100%
rename from shangdian/__init__.py
rename to rank/__init__.py
diff --git a/shangdian/admin.py b/rank/admin.py
similarity index 100%
rename from shangdian/admin.py
rename to rank/admin.py
diff --git a/dengji/apps.py b/rank/apps.py
similarity index 100%
rename from dengji/apps.py
rename to rank/apps.py
diff --git a/dengji/migrations/0001_initial.py b/rank/migrations/0001_initial.py
similarity index 100%
rename from dengji/migrations/0001_initial.py
rename to rank/migrations/0001_initial.py
diff --git a/dengji/migrations/0002_alter_dashoubiaoxian_dingdan.py b/rank/migrations/0002_alter_dashoubiaoxian_dingdan.py
similarity index 100%
rename from dengji/migrations/0002_alter_dashoubiaoxian_dingdan.py
rename to rank/migrations/0002_alter_dashoubiaoxian_dingdan.py
diff --git a/dengji/migrations/0003_alter_dashoubiaoxian_bankuai.py b/rank/migrations/0003_alter_dashoubiaoxian_bankuai.py
similarity index 100%
rename from dengji/migrations/0003_alter_dashoubiaoxian_bankuai.py
rename to rank/migrations/0003_alter_dashoubiaoxian_bankuai.py
diff --git a/dengji/migrations/0004_dingdanbiaoqian.py b/rank/migrations/0004_dingdanbiaoqian.py
similarity index 100%
rename from dengji/migrations/0004_dingdanbiaoqian.py
rename to rank/migrations/0004_dingdanbiaoqian.py
diff --git a/dengji/migrations/0005_fix_state.py b/rank/migrations/0005_fix_state.py
similarity index 100%
rename from dengji/migrations/0005_fix_state.py
rename to rank/migrations/0005_fix_state.py
diff --git a/dengji/migrations/0006_shenhejilu.py b/rank/migrations/0006_shenhejilu.py
similarity index 100%
rename from dengji/migrations/0006_shenhejilu.py
rename to rank/migrations/0006_shenhejilu.py
diff --git a/dengji/migrations/0007_kaoheguanbankuai.py b/rank/migrations/0007_kaoheguanbankuai.py
similarity index 100%
rename from dengji/migrations/0007_kaoheguanbankuai.py
rename to rank/migrations/0007_kaoheguanbankuai.py
diff --git a/dengji/migrations/0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more.py b/rank/migrations/0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more.py
similarity index 100%
rename from dengji/migrations/0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more.py
rename to rank/migrations/0008_chenghao_kaohe_guize_shenhejilu_dashou_beizhu_and_more.py
diff --git a/dengji/migrations/0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more.py b/rank/migrations/0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more.py
similarity index 100%
rename from dengji/migrations/0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more.py
rename to rank/migrations/0009_chenghao_kaioi_jinpai_alter_shenhejilu_jilu_id_and_more.py
diff --git a/dengji/migrations/0010_kaohejilufeiyong.py b/rank/migrations/0010_kaohejilufeiyong.py
similarity index 100%
rename from dengji/migrations/0010_kaohejilufeiyong.py
rename to rank/migrations/0010_kaohejilufeiyong.py
diff --git a/dengji/migrations/0011_kaohepaytemp.py b/rank/migrations/0011_kaohepaytemp.py
similarity index 100%
rename from dengji/migrations/0011_kaohepaytemp.py
rename to rank/migrations/0011_kaohepaytemp.py
diff --git a/shangdian/migrations/__init__.py b/rank/migrations/__init__.py
similarity index 100%
rename from shangdian/migrations/__init__.py
rename to rank/migrations/__init__.py
diff --git a/dengji/models.py b/rank/models.py
similarity index 99%
rename from dengji/models.py
rename to rank/models.py
index de3631a..0ebb16a 100644
--- a/dengji/models.py
+++ b/rank/models.py
@@ -1,6 +1,6 @@
from django.db import models
from django.conf import settings
-from dingdan.models import Dingdan
+from orders.models import Dingdan
# ==================== 板块表 ====================
class Bankuai(models.Model):
diff --git a/dengji/services.py b/rank/services.py
similarity index 100%
rename from dengji/services.py
rename to rank/services.py
diff --git a/shangdian/tests.py b/rank/tests.py
similarity index 100%
rename from shangdian/tests.py
rename to rank/tests.py
diff --git a/dengji/urls.py b/rank/urls.py
similarity index 100%
rename from dengji/urls.py
rename to rank/urls.py
diff --git a/dengji/utils.py b/rank/utils.py
similarity index 95%
rename from dengji/utils.py
rename to rank/utils.py
index 3e0c6b1..d115360 100644
--- a/dengji/utils.py
+++ b/rank/utils.py
@@ -1,24 +1,26 @@
-# dengji/utils.py
-from .models import Chenghao, KaoheCishuFeiyong, ShenheJilu, KaoheJiluFeiyong, YonghuChenghao, KaoheguanBankuai
-from yonghu.models import UserMain, UserShenheguan, UserDashou
-from django.db import models
-from decimal import Decimal
-from django.db.models import Q, Max
+# 标准库
import time
import random
import logging
-from django.db.models import F, Max
-logger = logging.getLogger('dengji')
+from decimal import Decimal
+# Django 内置
+from django.db import models
+from django.db.models import Q, F, Max
+# 项目模型
+## 当前dengji应用
+from .models import (
+ Chenghao, KaoheCishuFeiyong, ShenheJilu, KaoheJiluFeiyong,
+ YonghuChenghao, KaoheguanBankuai, DingdanBiaoqian
+)
+## users应用
+from users.models import UserMain, UserShenheguan, UserDashou
+## orders应用
+from orders.models import YonghuChenghao
+# 日志对象
+logger = logging.getLogger(__name__)
-
-
-# dingdan/utils.py
-from .models import DingdanBiaoqian
-from yonghu.models import UserMain
-from dengji.models import YonghuChenghao
-
def check_dashou_biaoqian_required(order_id, dashou_yonghuid):
"""
检查打手是否满足订单的标签需求。
@@ -60,8 +62,6 @@ def check_dashou_biaoqian_required(order_id, dashou_yonghuid):
except Exception as e:
# 标签校验失败不影响主流程,记录日志并保守放行(也可保守拒绝,按需求选择)
- import logging
- logger = logging.getLogger('dingdan')
logger.error(f"标签校验异常: {e}", exc_info=True)
# 安全起见,如果校验出错,选择拒绝,避免越权
return False, '订单标签校验异常,请稍后重试'
@@ -282,10 +282,6 @@ def create_shenhe_jilu_from_temp(user, temp_data):
参数 temp_data 是 KaohePayTemp 对象
返回 (success, message, record)
"""
- from .models import ShenheJilu, KaoheJiluFeiyong, Chenghao, YonghuChenghao
- from django.db.models import F
- import logging
- logger = logging.getLogger('dengji')
try:
logger.info(f"开始从临时表创建审核记录: 用户={user.yonghuid}, 订单={temp_data.dingdan_id}")
diff --git a/dengji/views.py b/rank/views.py
similarity index 96%
rename from dengji/views.py
rename to rank/views.py
index a26b81a..cfbd185 100644
--- a/dengji/views.py
+++ b/rank/views.py
@@ -1,15 +1,21 @@
-# dengji/views.py
-import logging
import json
+import logging
+from decimal import Decimal
+from django.db import models, transaction, IntegrityError
+from django.db.models import F, Q, OuterRef, Subquery, Count, Max
from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
+from rest_framework.permissions import IsAuthenticated
+from .utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
+from .models import (
+ YonghuChenghao, Chenghao, KaoheCishuFeiyong, KaoheJujueJilu,
+ KaoheguanBankuai, Bankuai, ShenheJilu, KaoheJiluFeiyong
+)
+from users.models import UserShenheguan, UserMain, UserDashou
+from products.models import ShangpinLeixing
-from shangpin.models import ShangpinLeixing
-from .models import YonghuChenghao, Chenghao, KaoheCishuFeiyong, KaoheJujueJilu
-from .utils import get_tag_fee, create_shenhe_jilu
-logger = logging.getLogger('dengji')
+logger = logging.getLogger(__name__)
class DashouBiaoqianView(APIView):
@@ -50,9 +56,6 @@ class DashouBiaoqianView(APIView):
return Response({'code': 1, 'msg': '服务器错误', 'data': None})
-
-from shangpin.models import ShangpinLeixing
-from .models import YonghuChenghao, Chenghao
class BankuaiBiaoqianView(APIView):
"""获取商品类型所属板块的所有标签"""
permission_classes = [IsAuthenticated]
@@ -91,18 +94,6 @@ class BankuaiBiaoqianView(APIView):
-
-# dengji/views.py
-import logging
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from .models import KaoheguanBankuai, Bankuai
-from django.db import transaction
-from yonghu.models import UserShenheguan, UserMain, UserDashou
-
-logger = logging.getLogger('dengji')
-
# 硬编码邀请码
KAOHEGUAN_INVITE_CODE = 'KHG2026VIP'
@@ -211,14 +202,6 @@ class KaoheguanRefreshView(APIView):
-import json
-from django.db import transaction, IntegrityError
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-
-from .models import Bankuai, ShenheJilu, KaoheguanBankuai, Chenghao
-
class ShenheDatingModuleView(APIView):
"""审核大厅获取模块列表(板块 + 待审核数量 + 是否关联)"""
permission_classes = [IsAuthenticated]
@@ -377,15 +360,7 @@ class ShenheQiangdanView(APIView):
-import json
-import logging
-from decimal import Decimal
-from django.db import transaction, models
-from django.db.models import F, Q, OuterRef, Subquery, Count, Max
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
class ShenheXiangqingView(APIView):
"""获取当前考核官审核中的记录详情(新增待收考核费字段)"""
@@ -684,10 +659,7 @@ class KaoheJinpaiTagsView(APIView):
except Exception as e:
return Response({'code': 500, 'msg': str(e)}, status=500)
-# dengji/utils.py
-from .models import Chenghao, KaoheCishuFeiyong, ShenheJilu, KaoheJiluFeiyong, YonghuChenghao
-from yonghu.models import UserMain
-from django.db import models
+
class KaoheJiluView(APIView):
"""获取打手本人的考核记录(支持筛选状态)"""
permission_classes = [IsAuthenticated]
@@ -762,7 +734,7 @@ class KaoheJiluView(APIView):
-from .utils import validate_shenheguan
+
class KaoheFreeApplyView(APIView):
"""免费申请考核(首次/再次,费用为0时调用)"""
permission_classes = [IsAuthenticated]
diff --git a/rp.py b/rp.py
new file mode 100644
index 0000000..d62c8b0
--- /dev/null
+++ b/rp.py
@@ -0,0 +1,80 @@
+import os
+
+# 目录定义
+ROOT_DIR = r"./"
+EXTRA_FILES = [
+ "./TransPyC.py",
+ "./StubGen.py",
+ "./Projectrans.py"
+]
+
+def snake_to_upper_camel(s: str) -> str:
+ if not s:
+ return s
+
+ if s.startswith('_'):
+ first = '_'
+ rest = s[1:]
+ else:
+ first = ''
+ rest = s
+
+ parts = rest.split('_')
+ pascal = ''.join(part.capitalize() for part in parts)
+ return first + pascal
+
+while True:
+ old_str = input("请输入原蛇形字符串:").strip()
+ new_input = input("替换内容(留空自动转大驼峰):").strip()
+
+ if not old_str:
+ print("错误:原字符串不能为空\n")
+ continue
+
+ total_mod = 0
+
+ # ====================== 1. 处理 lib 下所有 .py ======================
+ for root, _, files in os.walk(ROOT_DIR):
+ for file in files:
+ if file.endswith(".py"):
+ file_path = os.path.join(root, file)
+ try:
+ with open(file_path, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ if new_input == "":
+ replace_target = snake_to_upper_camel(old_str)
+ new_content = content.replace(old_str, replace_target)
+ else:
+ new_content = content.replace(old_str, new_input)
+
+ if new_content != content:
+ with open(file_path, "w", encoding="utf-8") as f:
+ f.write(new_content)
+ print(f"已修改:{file_path}")
+ total_mod += 1
+ except Exception as err:
+ print(f"异常 {file_path}:{err}")
+
+ # ====================== 2. 处理根目录下 3 个指定文件 ======================
+ for file_path in EXTRA_FILES:
+ if os.path.exists(file_path):
+ try:
+ with open(file_path, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ if new_input == "":
+ replace_target = snake_to_upper_camel(old_str)
+ new_content = content.replace(old_str, replace_target)
+ else:
+ new_content = content.replace(old_str, new_input)
+
+ if new_content != content:
+ with open(file_path, "w", encoding="utf-8") as f:
+ f.write(new_content)
+ print(f"已修改:{file_path}")
+ total_mod += 1
+ except Exception as err:
+ print(f"异常 {file_path}:{err}")
+
+ print(f"\n处理完成,共计修改 {total_mod} 个文件\n")
\ No newline at end of file
diff --git a/shangpin/__init__.py b/shop/__init__.py
similarity index 100%
rename from shangpin/__init__.py
rename to shop/__init__.py
diff --git a/shangpin/admin.py b/shop/admin.py
similarity index 100%
rename from shangpin/admin.py
rename to shop/admin.py
diff --git a/shangdian/apps.py b/shop/apps.py
similarity index 100%
rename from shangdian/apps.py
rename to shop/apps.py
diff --git a/shangdian/migrations/0001_initial.py b/shop/migrations/0001_initial.py
similarity index 100%
rename from shangdian/migrations/0001_initial.py
rename to shop/migrations/0001_initial.py
diff --git a/shangdian/migrations/0002_alter_yonghupingzheng_yonghu.py b/shop/migrations/0002_alter_yonghupingzheng_yonghu.py
similarity index 100%
rename from shangdian/migrations/0002_alter_yonghupingzheng_yonghu.py
rename to shop/migrations/0002_alter_yonghupingzheng_yonghu.py
diff --git a/shangdian/migrations/0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more.py b/shop/migrations/0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more.py
similarity index 100%
rename from shangdian/migrations/0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more.py
rename to shop/migrations/0003_dianpumorenpeizhi_dianpu_kaiqi_meiri_xiane_and_more.py
diff --git a/shangdian/migrations/0004_dianpushangpinshenheshezhi.py b/shop/migrations/0004_dianpushangpinshenheshezhi.py
similarity index 100%
rename from shangdian/migrations/0004_dianpushangpinshenheshezhi.py
rename to shop/migrations/0004_dianpushangpinshenheshezhi.py
diff --git a/shangdian/migrations/0005_dianpu_fenhong_choucheng_feilv_and_more.py b/shop/migrations/0005_dianpu_fenhong_choucheng_feilv_and_more.py
similarity index 100%
rename from shangdian/migrations/0005_dianpu_fenhong_choucheng_feilv_and_more.py
rename to shop/migrations/0005_dianpu_fenhong_choucheng_feilv_and_more.py
diff --git a/shangdian/migrations/0006_dianpushouzhimeiritongji_gei_shangji_cishu_and_more.py b/shop/migrations/0006_dianpushouzhimeiritongji_gei_shangji_cishu_and_more.py
similarity index 100%
rename from shangdian/migrations/0006_dianpushouzhimeiritongji_gei_shangji_cishu_and_more.py
rename to shop/migrations/0006_dianpushouzhimeiritongji_gei_shangji_cishu_and_more.py
diff --git a/shangpin/migrations/__init__.py b/shop/migrations/__init__.py
similarity index 100%
rename from shangpin/migrations/__init__.py
rename to shop/migrations/__init__.py
diff --git a/shangdian/models.py b/shop/models.py
similarity index 100%
rename from shangdian/models.py
rename to shop/models.py
diff --git a/shangpin/tests.py b/shop/tests.py
similarity index 100%
rename from shangpin/tests.py
rename to shop/tests.py
diff --git a/shangdian/urls.py b/shop/urls.py
similarity index 94%
rename from shangdian/urls.py
rename to shop/urls.py
index 0bca675..48ff274 100644
--- a/shangdian/urls.py
+++ b/shop/urls.py
@@ -1,6 +1,6 @@
from django.urls import path
-from shangdian.views import BindDianpuView, ShopGoodsView, ShangdianLoginView, ShopOverviewView, ShopQrcodeView, \
+from .views import BindDianpuView, ShopGoodsView, ShangdianLoginView, ShopOverviewView, ShopQrcodeView, \
ShopProductConfigView, ShopProductModifyView,ShopProductListView, ShopProductModifyView1,\
MemberListView,DianpuShouzhiTongjiView, QrcodeDownloadView, SubShopListView, SubShopModifyView,\
ShopOrderManageView,ShopOrderStatisticsView, ShopRefundView, ShopForceCompleteView,\
diff --git a/shangdian/utils.py b/shop/utils.py
similarity index 99%
rename from shangdian/utils.py
rename to shop/utils.py
index a5a0467..a4ab3bc 100644
--- a/shangdian/utils.py
+++ b/shop/utils.py
@@ -4,8 +4,8 @@ from rest_framework.response import Response
from .models import Dianpu, YonghuPingzheng
-from shangpin.models import Shangpin
-from shangdian.models import Dianpu, DianpuMorenPeizhi
+from products.models import Shangpin
+from .models import Dianpu, DianpuMorenPeizhi
# shangdian/utils.py 或 tongji/utils.py
@@ -14,7 +14,7 @@ from datetime import date
from decimal import Decimal
from django.db import transaction
from django.db.models import F
-from shangdian.models import Dianpu, DianpuShouzhiMeiriTongji
+from .models import Dianpu, DianpuShouzhiMeiriTongji
logger = logging.getLogger(__name__)
diff --git a/shangdian/views.py b/shop/views.py
similarity index 97%
rename from shangdian/views.py
rename to shop/views.py
index f07b496..3673e96 100644
--- a/shangdian/views.py
+++ b/shop/views.py
@@ -1,38 +1,60 @@
-
-from django.db import models
-
-from utils.oss_utils import upload_to_oss, delete_from_oss
-from yonghu.models import UserMain, UserDashou, UserShangjia, UserGuanshi, UserZuzhang
-from shangdian.models import Dianpu, YonghuDianpuBangding, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
-from shangpin.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin
-
-
-from rest_framework.permissions import AllowAny
-
-from django.utils import timezone
-from rest_framework_simplejwt.tokens import RefreshToken
-
-
-from yonghu.models import UserMain
-from .models import YonghuPingzheng, Dianpu
-
-
-import logging
-
-# shangdian/views.py 继续添加
+# ==================== 标准库 ====================
import io
+import json
import time
+import random
+import string
+import hmac
+import hashlib
import requests
+import logging
+import traceback
+import calendar
+import threading
+import xmltodict
+from decimal import Decimal
+from collections import defaultdict
+from concurrent.futures import ThreadPoolExecutor, as_completed
+
from django.conf import settings
+from django.db import models, transaction, IntegrityError
+from django.db.models import F, Sum, Count, Q, Prefetch
from django.core.cache import cache
-from django.db import transaction
+from django.core.paginator import Paginator, EmptyPage
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils import timezone
+from django.http import HttpResponse
+
from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
+from rest_framework.permissions import AllowAny, IsAuthenticated
+from rest_framework_simplejwt.tokens import RefreshToken
+
+from utils.oss_utils import upload_to_oss, delete_from_oss, get_oss_client
+
from .utils import verify_shop_permission
-from .models import Dianpu
-from django.db.models import F
+from shop.utils import verify_shop_permission, update_dianpu_daily_stat
+from products.utils import update_shangpin_daily_stat
+from orders.utils import update_daily_payout
+from backend.utils import update_dashou_daily_by_action
+
+from .models import YonghuPingzheng, Dianpu, DianpuShouzhiMeiriTongji
+
+from users.models import UserMain, UserDashou, UserShangjia, UserGuanshi, UserZuzhang
+
+from shop.models import YonghuDianpuBangding, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
+
+from products.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan
+
+from orders.models import (
+ Dingdan, DingdanPingtai, Dashoutupian, Tuikuanjilu,
+ OrderDashouHistory, CrossPlatformOrderData, Fadan
+)
+
+from config.models import ClubConfig, Club
+
+
logger = logging.getLogger(__name__)
@@ -438,10 +460,6 @@ class ShopOverviewView(APIView):
-# 新增导入
-from django.http import HttpResponse
-from utils.oss_utils import get_oss_client # 使用您已有的 COS 客户端获取方法
-
class QrcodeDownloadView(APIView):
"""
店铺二维码下载接口(通过后端代理下载,解决跨域限制)
@@ -492,10 +510,6 @@ class QrcodeDownloadView(APIView):
-# shangdian/views.py 追加
-from django.db.models import Sum
-
-from shangdian.models import DianpuShouzhiMeiriTongji
class DianpuShouzhiTongjiView(APIView):
"""
@@ -542,7 +556,6 @@ class DianpuShouzhiTongjiView(APIView):
).order_by('ri')
# 补全缺失的日期(默认值为0)
- import calendar
days_in_month = calendar.monthrange(year, month)[1]
result = []
for day in range(1, days_in_month + 1):
@@ -758,10 +771,6 @@ class ShopQrcodeView(APIView):
-from django.db.models import Count
-from rest_framework.views import APIView
-
-from shangpin.models import ShangpinLeixing
class ShopProductConfigView(APIView):
@@ -875,8 +884,6 @@ class ShopProductConfigView(APIView):
})
-from django.db import transaction
-from utils.oss_utils import delete_from_oss # 引入文件删除工具
class ShopProductModifyView(APIView):
@@ -1222,7 +1229,7 @@ class ShopProductModifyView(APIView):
return Response({'code': 500, 'msg': '删除失败,请稍后重试'})
-from django.core.paginator import Paginator
+
class ShopProductListView(APIView):
"""
商品列表接口,支持筛选、分页。
@@ -1718,7 +1725,7 @@ class ShopProductModifyView1(APIView):
return Response({'code': 0, 'msg': '商品已删除'})
-from shangpin.models import Huiyuan # 会员模型,根据实际路径调整
+
class MemberListView(APIView):
"""
获取所有会员列表(供商品发布时选择)
@@ -1763,12 +1770,6 @@ class MemberListView(APIView):
-
-from django.db import transaction, IntegrityError
-from django.core.paginator import Paginator, EmptyPage
-
-
-
class SubShopListView(APIView):
"""
获取下级店铺列表,同时返回上级分红汇总
@@ -2019,16 +2020,6 @@ class SubShopModifyView(APIView):
2. 订单列表:支持筛选、分页、返回订单详情及打手图片(含跨平台图片拉取)
"""
-from concurrent.futures import ThreadPoolExecutor, as_completed
-
-from django.db.models import Count, Q, Prefetch
-
-from django.core.paginator import Paginator, EmptyPage
- # 已存在的验证函数
-from dingdan.models import Dingdan, DingdanPingtai, Dashoutupian
-from peizhi.models import ClubConfig, Club # 跨平台相关模型,按实际路径调整
-
-from collections import defaultdict
# 如果有 ClubConfig 模型用于获取存储桶域名,确保导入(如没有可忽略,使用 settings 中的 OSS 域名)
@@ -2194,6 +2185,7 @@ class ShopOrderStatisticsView(APIView):
'code': 500,
'msg': '统计失败,服务器内部错误'
})
+
# ================================================================
# 辅助函数:获取OSS域名(用于拼接本地图片绝对URL)
# ================================================================
@@ -2542,28 +2534,6 @@ class ShopOrderManageView(APIView):
# shangdian/views/refund_view.py
# 店铺退款接口 —— 基于跨平台退款接口二改,只处理平台订单,我方派单
# ================================================================
-import json
-import time
-import random
-import string
-import hashlib
-import hmac
-import logging
-import traceback
-from decimal import Decimal
-import xmltodict # 解析微信退款回执
-import requests
-from django.db import transaction
-from django.conf import settings
-from django.core.exceptions import ObjectDoesNotExist
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-
-from shangdian.utils import verify_shop_permission # 店铺身份验证
-from dingdan.models import Dingdan, DingdanPingtai, Dashoutupian, Tuikuanjilu
-from yonghu.models import UserMain
-from shangdian.models import Dianpu
logger = logging.getLogger(__name__)
@@ -2647,7 +2617,6 @@ class ShopRefundView(APIView):
# 打手每日统计(退款动作)
try:
- from houtai.utils import update_dashou_daily_by_action
update_dashou_daily_by_action(
yonghuid=dashou_id,
amount=order.dashou_fencheng or Decimal('0.00'),
@@ -2658,15 +2627,12 @@ class ShopRefundView(APIView):
# 更新平台支出记录
try:
- from dingdan.utils import update_daily_payout
update_daily_payout(order.jine)
except Exception as e:
logger.error(f"更新每日支出失败: {e}")
# ========== 新增:商品和店铺每日统计 ==========
try:
- from shangdian.utils import update_dianpu_daily_stat
- from shangpin.utils import update_shangpin_daily_stat
# 商品每日统计(只要有商品ID就执行)
if order.shangpin_id:
# 从扩展表获取已计算好的收益值(下单时已存储)
@@ -2808,7 +2774,6 @@ class ShopRefundView(APIView):
此方法执行不影响主流程,任何异常仅记录日志。
"""
try:
- from dingdan.models import ClubConfig, Club, CrossPlatformOrderData
my_config = ClubConfig.objects.filter(is_self=1).first()
if not my_config:
logger.error("未配置我方俱乐部信息,无法通知对方")
@@ -2987,7 +2952,6 @@ class ShopForceCompleteView(APIView):
# ---------- 打手每日统计(成交) ----------
try:
- from houtai.utils import update_dashou_daily_by_action
update_dashou_daily_by_action(
yonghuid=dashou_id,
amount=dashou_fencheng,
@@ -3001,7 +2965,6 @@ class ShopForceCompleteView(APIView):
# 前提:订单创建时已存储了 dianpu_shouyi 到扩展表
if pingtai_ext.dianpu_shouyi:
try:
- from shangpin.utils import update_shangpin_daily_stat
# 商品每日统计(只要有商品ID就执行)
if order.shangpin_id:
# 从扩展表获取已计算好的收益值(下单时已存储)
@@ -3019,7 +2982,6 @@ class ShopForceCompleteView(APIView):
dianpu_zongshouyi=dianpu_shouyi
)
logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
- from shangdian.utils import update_dianpu_daily_stat
update_dianpu_daily_stat(
@@ -3061,7 +3023,6 @@ class ShopForceCompleteView(APIView):
partner_domain = club_rel.partner_domain.rstrip('/')
notify_url = f"{partner_domain}/houtai/dfddtk"
- import requests
payload = {
'order_id': partner_order_id,
'club_id': self_club_id,
@@ -3091,17 +3052,6 @@ class ShopForceCompleteView(APIView):
# shangdian/views/transfer_hall_view.py
# 店铺转移大厅接口 —— 添加本地打手退款统计
# ================================================================
-import logging
-import threading
-
-from django.core.exceptions import ObjectDoesNotExist
-
-from dingdan.models import Dingdan, DingdanPingtai, OrderDashouHistory
-
-from houtai.utils import update_dashou_daily_by_action # 打手每日统计(退款用)
-
-
-
class ShopTransferHallView(APIView):
"""
@@ -3215,7 +3165,6 @@ class ShopTransferHallView(APIView):
# 获取我方俱乐部ID
my_club_config = None
try:
- from peizhi.models import ClubConfig
my_club_config = ClubConfig.objects.filter(is_self=1).first()
except Exception as e:
logger.error(f"获取俱乐部配置失败: {e}")
@@ -3224,8 +3173,6 @@ class ShopTransferHallView(APIView):
self_club_id = my_club_config.club_id
def notify():
try:
- from dingdan.models import CrossPlatformOrderData
- from peizhi.models import Club # 确保导入
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
partner_domain = club_rel.partner_domain.rstrip('/')
notify_url = f"{partner_domain}/houtai/kpttzzydt"
@@ -3284,10 +3231,6 @@ class ShopTransferHallView(APIView):
# shangdian/views/fine_apply_view.py
# 店铺罚款申请接口 —— 二改自 FineApplyView(客服罚款)
# ================================================================
-
-from dingdan.models import Dingdan, DingdanPingtai, Fadan # 订单、罚款记录
-
-
class ShopFineApplyView(APIView):
"""
店铺罚款申请接口
diff --git a/yonghu/__init__.py b/users/__init__.py
similarity index 100%
rename from yonghu/__init__.py
rename to users/__init__.py
diff --git a/yonghu/admin.py b/users/admin.py
similarity index 100%
rename from yonghu/admin.py
rename to users/admin.py
diff --git a/yonghu/apps.py b/users/apps.py
similarity index 100%
rename from yonghu/apps.py
rename to users/apps.py
diff --git a/yonghu/fadan_fenhong_utils.py b/users/fadan_fenhong_utils.py
similarity index 98%
rename from yonghu/fadan_fenhong_utils.py
rename to users/fadan_fenhong_utils.py
index c816b50..ddc4352 100644
--- a/yonghu/fadan_fenhong_utils.py
+++ b/users/fadan_fenhong_utils.py
@@ -7,8 +7,8 @@ from decimal import Decimal
from django.db import transaction
from django.core.exceptions import ObjectDoesNotExist
-from dingdan.models import Fadan, FadanFenhong, FadanFenhongLilv
-from yonghu.models import UserShangjia
+from orders.models import Fadan, FadanFenhong, FadanFenhongLilv
+from users.models import UserShangjia
logger = logging.getLogger(__name__)
diff --git a/yonghu/migrations/0001_initial.py b/users/migrations/0001_initial.py
similarity index 100%
rename from yonghu/migrations/0001_initial.py
rename to users/migrations/0001_initial.py
diff --git a/yonghu/migrations/0002_adminprofile_yaoqingma.py b/users/migrations/0002_adminprofile_yaoqingma.py
similarity index 100%
rename from yonghu/migrations/0002_adminprofile_yaoqingma.py
rename to users/migrations/0002_adminprofile_yaoqingma.py
diff --git a/yonghu/migrations/0003_rankingrecord.py b/users/migrations/0003_rankingrecord.py
similarity index 100%
rename from yonghu/migrations/0003_rankingrecord.py
rename to users/migrations/0003_rankingrecord.py
diff --git a/yonghu/migrations/0004_alter_usermain_avatar.py b/users/migrations/0004_alter_usermain_avatar.py
similarity index 100%
rename from yonghu/migrations/0004_alter_usermain_avatar.py
rename to users/migrations/0004_alter_usermain_avatar.py
diff --git a/yonghu/migrations/0005_officialaccountuser_usermain_unionid.py b/users/migrations/0005_officialaccountuser_usermain_unionid.py
similarity index 100%
rename from yonghu/migrations/0005_officialaccountuser_usermain_unionid.py
rename to users/migrations/0005_officialaccountuser_usermain_unionid.py
diff --git a/yonghu/migrations/0006_alter_officialaccountuser_unionid.py b/users/migrations/0006_alter_officialaccountuser_unionid.py
similarity index 100%
rename from yonghu/migrations/0006_alter_officialaccountuser_unionid.py
rename to users/migrations/0006_alter_officialaccountuser_unionid.py
diff --git a/yonghu/migrations/0007_userkefu.py b/users/migrations/0007_userkefu.py
similarity index 100%
rename from yonghu/migrations/0007_userkefu.py
rename to users/migrations/0007_userkefu.py
diff --git a/yonghu/migrations/0008_userguanshi_fenghong_erci_and_more.py b/users/migrations/0008_userguanshi_fenghong_erci_and_more.py
similarity index 100%
rename from yonghu/migrations/0008_userguanshi_fenghong_erci_and_more.py
rename to users/migrations/0008_userguanshi_fenghong_erci_and_more.py
diff --git a/yonghu/migrations/0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more.py b/users/migrations/0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more.py
similarity index 100%
rename from yonghu/migrations/0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more.py
rename to users/migrations/0009_userdashou_ewai_xiane_userdashou_jinritixian_jine_and_more.py
diff --git a/yonghu/migrations/0010_userguanshi_invite_qrcode_url.py b/users/migrations/0010_userguanshi_invite_qrcode_url.py
similarity index 100%
rename from yonghu/migrations/0010_userguanshi_invite_qrcode_url.py
rename to users/migrations/0010_userguanshi_invite_qrcode_url.py
diff --git a/yonghu/migrations/0011_usermain_shoujihao_renzheng.py b/users/migrations/0011_usermain_shoujihao_renzheng.py
similarity index 100%
rename from yonghu/migrations/0011_usermain_shoujihao_renzheng.py
rename to users/migrations/0011_usermain_shoujihao_renzheng.py
diff --git a/yonghu/migrations/0012_usershenheguan.py b/users/migrations/0012_usershenheguan.py
similarity index 100%
rename from yonghu/migrations/0012_usershenheguan.py
rename to users/migrations/0012_usershenheguan.py
diff --git a/yonghu/migrations/0013_usershenheguan_is_renzheng.py b/users/migrations/0013_usershenheguan_is_renzheng.py
similarity index 100%
rename from yonghu/migrations/0013_usershenheguan_is_renzheng.py
rename to users/migrations/0013_usershenheguan_is_renzheng.py
diff --git a/yonghu/migrations/0014_tixianautorecord_tixianshenhejilu_and_more.py b/users/migrations/0014_tixianautorecord_tixianshenhejilu_and_more.py
similarity index 100%
rename from yonghu/migrations/0014_tixianautorecord_tixianshenhejilu_and_more.py
rename to users/migrations/0014_tixianautorecord_tixianshenhejilu_and_more.py
diff --git a/yonghu/migrations/__init__.py b/users/migrations/__init__.py
similarity index 100%
rename from yonghu/migrations/__init__.py
rename to users/migrations/__init__.py
diff --git a/yonghu/models.py b/users/models.py
similarity index 100%
rename from yonghu/models.py
rename to users/models.py
diff --git a/yonghu/paihang_views.py b/users/paihang_views.py
similarity index 98%
rename from yonghu/paihang_views.py
rename to users/paihang_views.py
index 6b686a7..d24ea42 100644
--- a/yonghu/paihang_views.py
+++ b/users/paihang_views.py
@@ -11,13 +11,13 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
-from houtai.models import (
+from backend.models import (
DashouRiTongji,
GuanshiRiTongji,
ZuzhangRiTongji,
ShangjiaRiTongji,
)
-from yonghu.models import UserMain, UserDashou, UserShangjia, UserBoss
+from users.models import UserMain, UserDashou, UserShangjia, UserBoss
logger = logging.getLogger(__name__)
diff --git a/yonghu/ranking_tasks.py b/users/ranking_tasks.py
similarity index 99%
rename from yonghu/ranking_tasks.py
rename to users/ranking_tasks.py
index 4614f9f..86d29d0 100644
--- a/yonghu/ranking_tasks.py
+++ b/users/ranking_tasks.py
@@ -11,7 +11,7 @@ from datetime import datetime, timedelta
import logging
from decimal import Decimal
-from yonghu.models import (
+from users.models import (
UserMain, UserDashou, UserShangjia, UserGuanshi, UserBoss,
RankingRecord
)
diff --git a/yonghu/tasks.py b/users/tasks.py
similarity index 98%
rename from yonghu/tasks.py
rename to users/tasks.py
index ce07131..b46ac27 100644
--- a/yonghu/tasks.py
+++ b/users/tasks.py
@@ -12,7 +12,7 @@ from datetime import datetime
import logging
# 导入模型
-from yonghu.models import UserDashou, UserShangjia, UserGuanshi
+from users.models import UserDashou, UserShangjia, UserGuanshi
from utils.celery_utils import log_task_execution, rollback_on_failure
logger = logging.getLogger(__name__)
diff --git a/yonghu/tests.py b/users/tests.py
similarity index 100%
rename from yonghu/tests.py
rename to users/tests.py
diff --git a/yonghu/tixian_shenhe_services.py b/users/tixian_shenhe_services.py
similarity index 99%
rename from yonghu/tixian_shenhe_services.py
rename to users/tixian_shenhe_services.py
index 1b0e5cd..9ce1d7c 100644
--- a/yonghu/tixian_shenhe_services.py
+++ b/users/tixian_shenhe_services.py
@@ -25,11 +25,16 @@ from django.conf import settings
from django.db import transaction
from django.utils import timezone
-from dingdan.models import Dingdan, DingdanShangjia, Fadan, Lilubiao
-from peizhi.models import Szjilu, TixianQuotaDefault
+from orders.models import Dingdan, DingdanShangjia, Fadan, Lilubiao
+from config.models import Szjilu, TixianQuotaDefault
from utils.fadan_utils import check_fadan_qiangdan_eligible
+from products.models import Huiyuangoumai
+from backend.models import TixianRiTongji
+
+from utils.wechat_v3 import build_authorization
+
from .models import (
TixianAutoRecord,
TixianShenheJilu,
@@ -42,6 +47,10 @@ from .models import (
UserZuzhang,
)
+from orders.utils import update_daily_payout
+
+from django.db.models import Sum
+
logger = logging.getLogger('yonghu.tixian_shenhe')
ORDER_STATUS_COMPLETED = 3
@@ -159,7 +168,6 @@ def check_dashou_has_valid_huiyuan(yonghuid):
打手至少持有一个未过期会员(Huiyuangoumai + jiance_shifou_daoqi 同步状态)
返回 (True, '') 或 (False, 原因)
"""
- from shangpin.models import Huiyuangoumai
records = Huiyuangoumai.objects.filter(yonghu_id=yonghuid)
if not records.exists():
@@ -423,8 +431,6 @@ def _get_active_auto_record(shenhe_danhao):
def _get_platform_daily_total(leixing, today=None):
"""平台今日已提现总额:只读 houtai.TixianRiTongji(到账金额口径)"""
- from houtai.models import TixianRiTongji
-
if today is None:
today = date.today()
try:
@@ -559,7 +565,6 @@ def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe
本审核单已有进行中/成功打款记录则视为已预占,不重复加计。
返回 (True, '', '') 或 (False, msg, 'personal'|'platform')
"""
- from houtai.models import TixianRiTongji
today = date.today()
shijidaozhang = shijidaozhang or decimal.Decimal('0.00')
@@ -637,8 +642,6 @@ def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe
def release_collect_quota_reservation(user_main, leixing, jine, shijidaozhang):
"""微信打款明确失败/用户取消时回滚收款预占的限额"""
- from houtai.models import TixianRiTongji
-
today = date.today()
shijidaozhang = shijidaozhang or decimal.Decimal('0.00')
try:
@@ -706,7 +709,6 @@ def _quota_already_reserved_for_record(auto_record):
"""判断本笔打款是否已在收款预占时写入日统计(避免成功时重复累加)"""
if not auto_record.shenhe_danhao:
return False
- from django.db.models import Sum
today = date.today()
shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00')
@@ -730,9 +732,6 @@ def apply_transfer_success_quota(auto_record):
"""
if not auto_record.shenhe_danhao:
return
-
- from houtai.models import TixianRiTongji
-
today = date.today()
leixing = auto_record.leixing
jine = auto_record.jine
@@ -917,7 +916,6 @@ def query_wechat_transfer_bill(out_bill_no):
GET /v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}
返回 dict;网络/系统异常返回 None;微信无此单返回 {'_not_found': True}
"""
- from utils.wechat_v3 import build_authorization
url = (
'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/'
@@ -961,7 +959,6 @@ def _parse_package_info(raw):
def _apply_platform_accounting(auto_record):
"""平台收支统计(幂等由 mark_transfer_success 外层保证只调一次)"""
- from dingdan.utils import update_daily_payout
update_daily_payout(auto_record.shijidaozhang)
try:
diff --git a/yonghu/tixian_shenhe_views.py b/users/tixian_shenhe_views.py
similarity index 98%
rename from yonghu/tixian_shenhe_views.py
rename to users/tixian_shenhe_views.py
index 6e1b8a1..2fd8c31 100644
--- a/yonghu/tixian_shenhe_views.py
+++ b/users/tixian_shenhe_views.py
@@ -19,7 +19,10 @@ from rest_framework import permissions
from rest_framework.response import Response
from rest_framework.views import APIView
-from .models import TixianAutoRecord, TixianShenheJilu
+from utils.redis_lock import acquire_lock, release_lock
+from utils.wechat_v3 import build_authorization
+
+from .models import TixianAutoRecord
from .tixian_shenhe_services import (
RECONCILE_ALLOW_NEW,
RECONCILE_COMPLETED,
@@ -113,8 +116,6 @@ def _build_wx_transfer_body(wx_cfg, tixian_id, openid, yonghuid, leixing, shijid
def _call_wechat_transfer(body, wx_cfg):
- from utils.wechat_v3 import build_authorization
-
url = 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills'
body_json = json.dumps(body, separators=(',', ':'))
auth = build_authorization('POST', url, body_json)
@@ -137,8 +138,6 @@ class TixianZddkshApplyView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
- from utils.redis_lock import acquire_lock, release_lock
-
user_main = request.user
lock_key = f'tixian_audit_apply:{user_main.yonghuid}'
identifier = acquire_lock(lock_key, timeout=10)
@@ -184,8 +183,6 @@ def process_audit_collect(request):
若 TixianAutoRecord 已有 tixian_id → 只问微信,非终态绝不新建
仅微信 FAIL/CANCELLED 终态后才允许新建打款记录
"""
- from utils.redis_lock import acquire_lock, release_lock
-
user_main = request.user
yonghuid = user_main.yonghuid
tixianjilu_id = request.data.get('tixianjilu_id') or request.data.get('id')
diff --git a/yonghu/urls.py b/users/urls.py
similarity index 100%
rename from yonghu/urls.py
rename to users/urls.py
diff --git a/yonghu/views.py b/users/views.py
similarity index 96%
rename from yonghu/views.py
rename to users/views.py
index b626da0..4c45ee3 100644
--- a/yonghu/views.py
+++ b/users/views.py
@@ -1,13558 +1,12885 @@
-# yonghu/views.py
-import time
-import random
-import hashlib
-import requests
-from django.db import transaction
-from django.conf import settings
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework_simplejwt.tokens import RefreshToken
-
-from .models import UserMain, UserBoss, UserDashou, UserShangjia, UserGuanshi
-from dingdan.models import Dingdan, DingdanPingtai
-from peizhi.models import Qunpeizhi # 假设群配置表在这个模块
-from rest_framework.throttling import AnonRateThrottle
-from rest_framework.permissions import AllowAny # 新增这行
-
-
-
-from houtai.utils import update_dashou_daily_by_action
-
-
-
-
-
-from django.conf import settings
-from django.utils import timezone
-from django.db import transaction
-from django.core.exceptions import ObjectDoesNotExist
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.throttling import AnonRateThrottle
-from rest_framework.permissions import AllowAny
-from rest_framework_simplejwt.tokens import RefreshToken
-from .models import UserMain, AdminProfile
-
-
-
-
-
-# 获取用户代理信息(用于更详细的追踪)
-def huoquYonghuDaili(request):
- """
- 获取用户浏览器/设备信息
- """
- user_agent = request.META.get('HTTP_USER_AGENT', '')
-
- # 这里可以添加更复杂的用户代理解析
- # 为简化,我们只返回原始字符串的前100个字符
- return user_agent[:100] if user_agent else ''
-
-
-
-
-
-
-import ipaddress
-
-def huoquKehuduanIP(request):
- """
- 强化版:安全、兼容地获取客户端真实IP地址
- """
- ip = None
-
- # 1. 从标准头部获取IP链
- x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
- if x_forwarded_for:
- # 按逗号分割,取第一个(即最原始的客户端IP)
- ip = x_forwarded_for.split(',')[0].strip()
-
- # 2. 如果上面没有,则用Nginx传递的X-Real-IP
- if not ip:
- ip = request.META.get('HTTP_X_REAL_IP')
-
- # 3. 最后降级到连接地址
- if not ip:
- ip = request.META.get('REMOTE_ADDR')
-
- # ===== 核心:清洗和验证IP格式 =====
- if ip:
- # 情况A: 处理IPv4映射的IPv6地址 (::ffff:192.168.1.1)
- if ip.startswith('::ffff:'):
- ip = ip[7:]
-
- # 情况B: 处理带端口的IPv4地址 (223.91.110.123:62417)
- # 这是导致您记录“三段”的最可能原因!
- # 如果IP字符串中有冒号,且最后一个冒号前的部分是IPv4,则分离端口。
- if ':' in ip and '.' in ip:
- # 尝试按冒号分割,取第一部分
- possible_ip = ip.rsplit(':', 1)[0] # 从右边分割一次
- # 验证它是否是有效的IPv4地址
- try:
- ipaddress.IPv4Address(possible_ip) # 如果通过验证,说明格式正确
- ip = possible_ip
- except (ipaddress.AddressValueError, ValueError):
- # 如果验证失败,说明可能不是IPv4带端口,保持原样或进行其他处理
- pass
-
- # 情况C: 验证最终结果是否为有效IPv4(可选,但推荐)
- try:
- # 此步骤会抛出异常,如果ip变量此时仍是“223.91.110.”这种格式
- ipaddress.IPv4Address(ip)
- except (ipaddress.AddressValueError, ValueError):
- # 记录一个警告日志,并将IP标记为无效或存为原始字符串
- print(f"警告:获取到无效或非标准IP地址格式: {ip}")
- # 您可以选择在此处 return None 或存储原始字符串以供排查
-
- return ip
-
-
-
-
-
-
-from yonghu.models import UserShenheguan
-
-class WechatMiniProgramLoginView(APIView):
- """
- 微信小程序登录接口
- """
- throttle_classes = [AnonRateThrottle]
- permission_classes = [AllowAny]
-
- def post(self, request):
- try:
- code = request.data.get('code', '').strip()
- if not code:
- return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 获取微信openid和unionid
- wechat_data = self.get_wechat_openid(code)
- if not wechat_data or 'openid' not in wechat_data:
- error_msg = wechat_data.get('errmsg', '微信授权失败')
- return Response({'code': 2, 'msg': f'微信登录失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- openid = wechat_data['openid']
- session_key = wechat_data.get('session_key', '')
- unionid = wechat_data.get('unionid', '') # 🆕【新增】获取unionid
-
- # 获取客户端真实IP
- kehuduan_ip = self.huoquKehuduanIP(request)
-
- with transaction.atomic():
- # 创建或获取用户
- user_main, created = UserMain.objects.select_for_update().get_or_create(
- openid=openid,
- defaults={'yonghuid': self.shengchengYonghuID(), 'user_type': 'normal'}
- )
-
- # 🆕【新增】保存unionid
- if unionid and unionid != user_main.unionid:
- user_main.unionid = unionid
-
- cunchu_ip = kehuduan_ip
- user_main.ip = cunchu_ip
- user_main.last_login_time = timezone.now()
- user_main.save()
-
- if created:
- UserBoss.objects.create(user=user_main, nickname='板板大人')
-
- # 检查扩展表
- dashou_status = 0
- shangjia_status = 0
- guanshi_status = 0
- boss_nickname = '微信用户'
-
- try:
- boss_profile = UserBoss.objects.get(user=user_main)
- boss_nickname = boss_profile.nickname if boss_profile.nickname else '微信用户'
- except UserBoss.DoesNotExist:
- pass
-
- try:
- UserDashou.objects.get(user=user_main)
- dashou_status = 1
- except UserDashou.DoesNotExist:
- dashou_status = None
-
- try:
- UserShangjia.objects.get(user=user_main)
- shangjia_status = 1
- except UserShangjia.DoesNotExist:
- shangjia_status = None
-
- try:
- UserGuanshi.objects.get(user=user_main)
- guanshi_status = 1
- except UserGuanshi.DoesNotExist:
- guanshi_status = None
-
-
- try:
- UserZuzhang.objects.get(user=user_main)
- zuzhang_status = 1
- except UserZuzhang.DoesNotExist:
- zuzhang_status = None
-
-
- # 在检查 zuzhang 扩展表之后,插入以下代码
- try:
- kg = UserShenheguan.objects.get(user=user_main)
- kaoheguan_status = 1 if kg.zhuangtai == 1 else 0
- except UserShenheguan.DoesNotExist:
- kaoheguan_status = 0
-
- # 生成JWT token
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
-
- # 计算订单数量
- order_counts = self.jisuanDingdanShuliang(user_main.yonghuid)
-
- # 获取群配置
- group_info = self.huoquQunPeizhi()
-
- # 返回数据
- response_data = {
- 'token': token,
- 'nicheng': boss_nickname,
- 'uid': user_main.yonghuid,
- 'touxiang': user_main.avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- 'zuzhangstatus': zuzhang_status,
- 'kaoheguanstatus': kaoheguan_status,
- 'dingdantiaoshu': order_counts,
- 'dashouqun': group_info.get('dashouqun', ''),
- 'dashouqunid': group_info.get('dashouqunid', ''),
- 'guanshiqun': group_info.get('guanshiqun', ''),
- 'guanshiqunid': group_info.get('guanshiqunid', '')
- }
-
- return Response({'code': 0, 'msg': '登录成功', 'data': response_data})
-
- except Exception as e:
- import logging
- logger = logging.getLogger(__name__)
- error_openid = locals().get('openid', 'N/A')
- logger.error(f"微信登录异常 - openid: {error_openid}, 错误类型: {type(e).__name__}, 错误详情: {str(e)}",
- exc_info=True)
-
- return Response({'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def get_wechat_openid(self, code):
- """
- 调用微信接口获取openid和unionid
- """
- try:
- appid = getattr(settings, 'WEIXIN_APPID', '')
- secret = getattr(settings, 'WEIXIN_SECRET', '')
-
- if not appid or not secret:
- raise ValueError('微信配置未设置')
-
- url = 'https://api.weixin.qq.com/sns/jscode2session'
- params = {'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code'}
-
- response = requests.get(url, params=params, timeout=10)
- result = response.json()
-
- if 'openid' in result:
- return {'openid': result['openid'], 'session_key': result.get('session_key', ''),
- 'unionid': result.get('unionid', '')}
- else:
- errcode = result.get('errcode', 'unknown')
- errmsg = result.get('errmsg', '未知错误')
- return {'errmsg': f'[{errcode}]{errmsg}'}
-
- except requests.exceptions.Timeout:
- return {'errmsg': '请求微信接口超时'}
- except Exception as e:
- return {'errmsg': f'请求微信接口异常: {str(e)}'}
-
- def shengchengYonghuID(self):
- """
- 生成7位数字用户ID
- """
- for _ in range(10):
- timestamp_part = str(int(time.time()))[-5:].zfill(5)
- random_part = str(random.randint(0, 99)).zfill(2)
- user_id = timestamp_part + random_part
-
- if len(user_id) == 7 and user_id.isdigit():
- if not UserMain.objects.filter(yonghuid=user_id).exists():
- return user_id
-
- raise Exception('生成用户ID失败')
-
- def huoquKehuduanIP(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
-
- ip = request.META.get('REMOTE_ADDR', '')
- return ip if ip else '0.0.0.0'
-
- def jisuanDingdanShuliang(self, yonghuid):
- """
- 统计订单数量
- """
- try:
- if not DingdanPingtai.objects.filter(laoban_id=yonghuid).exists():
- return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0}
-
- dingdan_ids = DingdanPingtai.objects.filter(laoban_id=yonghuid).values_list('dingdan__id', flat=True)
-
- from django.db.models import Q, Count
- from django.db.models import Case, When, IntegerField
-
- results = Dingdan.objects.filter(id__in=dingdan_ids).aggregate(
- daifuwu=Count(Case(When(Q(zhuangtai=1) | Q(zhuangtai=7), then=1), output_field=IntegerField())),
- fuwuzhong=Count(Case(When(zhuangtai=2, then=1), output_field=IntegerField())),
- yiwancheng=Count(Case(When(zhuangtai=3, then=1), output_field=IntegerField())),
- yituikuan=Count(Case(When(zhuangtai=5, then=1), output_field=IntegerField()))
- )
-
- return {
- 'daifuwu': results['daifuwu'] or 0,
- 'fuwuzhong': results['fuwuzhong'] or 0,
- 'yiwancheng': results['yiwancheng'] or 0,
- 'yituikuan': results['yituikuan'] or 0
- }
-
- except Exception:
- return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0}
-
- def huoquQunPeizhi(self):
- """
- 获取群配置信息
- """
- try:
- qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2])
-
- result = {
- 'dashouqun': '',
- 'dashouqunid': '',
- 'guanshiqun': '',
- 'guanshiqunid': ''
- }
-
- for config in qun_configs:
- if config.id == 1:
- result['dashouqun'] = config.neirong or ''
- result['dashouqunid'] = config.qunid or ''
- elif config.id == 2:
- result['guanshiqun'] = config.neirong or ''
- result['guanshiqunid'] = config.qunid or ''
-
- return result
-
- except Exception:
- return {
- 'dashouqun': '',
- 'dashouqunid': '',
- 'guanshiqun': '',
- 'guanshiqunid': ''
- }
-
-# views/weixin_official.py - 完整文件
-import hashlib
-import time
-import logging
-from django.views.decorators.csrf import csrf_exempt
-from django.http import HttpResponse
-from django.utils import timezone
-import defusedxml.ElementTree as ET
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from django.conf import settings
-from .models import OfficialAccountUser
-
-logger = logging.getLogger(__name__)
-
-
-
-class WeixinOfficialCallbackView(APIView):
- """
- 微信服务号回调接口
- 处理用户关注、取消关注等事件
- 核心:有关注事件就存,不管有没有UnionID
- """
- throttle_classes = []
- permission_classes = [AllowAny]
- authentication_classes = []
-
- def get(self, request):
- """验证服务器配置(微信官方要求)"""
- try:
- signature = request.GET.get('signature', '')
- timestamp = request.GET.get('timestamp', '')
- nonce = request.GET.get('nonce', '')
- echostr = request.GET.get('echostr', '')
-
- token = getattr(settings, 'WEIXIN_OFFICIAL_TOKEN', '')
-
- if not token:
- logger.error("服务号Token未配置")
- return HttpResponse('Token not configured', status=500)
-
- # 验证签名
- tmp_list = sorted([token, timestamp, nonce])
- tmp_str = ''.join(tmp_list)
- hash_str = hashlib.sha1(tmp_str.encode()).hexdigest()
-
- if hash_str == signature:
- return HttpResponse(echostr)
- else:
- logger.error(f"签名验证失败")
- return HttpResponse('Signature verification failed', status=403)
-
- except Exception as e:
- logger.error(f"验证服务器异常: {str(e)}")
- return HttpResponse('Internal Server Error', status=500)
-
- def post(self, request):
- """处理事件推送(关注、取关)"""
- try:
- xml_str = request.body.decode('utf-8')
- logger.info(f"收到服务号事件,原始数据(前200字符): {xml_str[:200]}")
-
- root = ET.fromstring(xml_str)
-
- msg_type = root.find('MsgType').text
- from_user = root.find('FromUserName').text # 服务号OpenID
-
- if msg_type == 'event':
- event = root.find('Event').text
-
- if event == 'subscribe':
- return self.handle_subscribe_event(root, from_user)
- elif event == 'unsubscribe':
- return self.handle_unsubscribe_event(root, from_user)
- else:
- # 其他事件(如菜单点击)直接返回success
- return HttpResponse('success')
-
- # 非事件消息也直接返回success
- return HttpResponse('success')
-
- except Exception as e:
- logger.error(f"处理服务号事件异常: {str(e)}", exc_info=True)
- # 即使异常也返回success,避免微信服务器重试
- return HttpResponse('success')
-
- def handle_subscribe_event(self, root, from_user):
- """处理用户关注事件 - 简化版:有就存,没有拉倒"""
- try:
- # 尝试获取UnionID(有就有,没有也没关系)
- unionid_elem = root.find('UnionId')
- unionid = unionid_elem.text if unionid_elem is not None else None
-
- logger.info(f"用户关注,OpenID: {from_user}, UnionID: {unionid}")
-
- # 核心逻辑:更新或创建记录
- obj, created = OfficialAccountUser.objects.update_or_create(
- official_openid=from_user,
- defaults={
- 'unionid': unionid,
- 'is_subscribed': True,
- 'subscribe_time': timezone.now()
- }
- )
-
- action = "新建" if created else "更新"
- logger.info(f"{action}用户记录成功: OpenID={from_user}")
-
- # 回复欢迎消息(可选)
- return self.generate_text_response(root, "感谢关注星阙网络服务!平台有新订单时您会收到通知。")
-
- except Exception as e:
- logger.error(f"处理关注事件异常: {str(e)}", exc_info=True)
- # 依然返回success,不让微信重试
- return HttpResponse('success')
-
- def handle_unsubscribe_event(self, root, from_user):
- """处理用户取消关注事件 - 简化版:标记为未订阅"""
- try:
- updated_count = OfficialAccountUser.objects.filter(
- official_openid=from_user
- ).update(is_subscribed=False)
-
- if updated_count > 0:
- logger.info(f"用户取消关注,已标记为未订阅: {from_user}")
- else:
- logger.info(f"用户取消关注,但记录不存在(无需处理): {from_user}")
-
- return HttpResponse('success')
-
- except Exception as e:
- logger.error(f"处理取消关注事件异常: {str(e)}", exc_info=True)
- return HttpResponse('success')
-
- def generate_text_response(self, root, content):
- """生成文本消息响应"""
- try:
- to_user = root.find('ToUserName').text
- from_user = root.find('FromUserName').text
-
- response_xml = f"""
-
-
-{int(time.time())}
-
-
-"""
-
- return HttpResponse(response_xml, content_type='application/xml')
-
- except Exception as e:
- logger.error(f"生成回复消息异常: {str(e)}")
- # 即使生成回复失败,也返回一个空的success
- return HttpResponse('success')
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-# yonghu/views.py
-from rest_framework.parsers import MultiPartParser, FormParser,JSONParser
-from rest_framework import permissions
-from django.db import transaction
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.views import APIView
-from .models import UserMain, UserBoss, UserDashou, UserShangjia, UserGuanshi
-from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
-import os
-import uuid
-
-import hashlib
-from django.core.cache import cache
-
-
-
-
-
-
-
-
-class UserInfoUpdateView(APIView):
- permission_classes = [permissions.IsAuthenticated]
-
- # 进程内缓存,防止同一code被重复调用微信(单进程安全,多进程小概率重复,但用户极少可忽略)
- _phone_cache = {}
-
- def post(self, request):
- try:
- user = request.user
- boss = self._get_or_create_boss(user)
- result = {}
-
- # 头像
- av = request.FILES.get('avatar')
- if av:
- ok, msg = validate_image(av)
- if not ok:
- return Response({'code': 1, 'msg': msg, 'data': None}, status=400)
- url = self._upload_avatar(user, av)
- if url:
- result['touxiang'] = url
- user.avatar = url
- else:
- return Response({'code': 2, 'msg': '头像上传失败', 'data': None}, status=500)
-
- # 昵称
- nick = request.data.get('nicheng')
- if nick:
- nick = str(nick).strip()
- if len(nick) > 20:
- return Response({'code': 3, 'msg': '昵称过长', 'data': None}, status=400)
- boss.nickname = nick
- result['nicheng'] = nick
-
- # 手机号 —— 极简可靠版
- code = request.data.get('shoujihao_code')
- if code:
- phone = self._get_phone_safe(code)
- if phone is None:
- return Response({'code': 6, 'msg': '手机号获取失败,请重新授权', 'data': None}, status=400)
- user.phone = phone
- user.shoujihao_renzheng = True
- result['phone'] = phone
-
- if not result:
- return Response({'code': 5, 'msg': '无更新', 'data': None}, status=400)
-
- with transaction.atomic():
- user.save()
- boss.save()
- return Response({'code': 0, 'msg': '更新成功', 'data': result})
-
- except Exception as e:
- logger.exception("系统异常")
- return Response({'code': 99, 'msg': str(e), 'data': None}, status=500)
-
- # ---------- 安全手机号(内存去重) ----------
- def _get_phone_safe(self, code):
- # 如果已经处理过这个code,直接返回结果
- if code in self._phone_cache:
- cached = self._phone_cache[code]
- return cached if cached != '__FAIL__' else None
-
- # 标记处理中(简单防重入)
- self._phone_cache[code] = '__PROCESSING__'
- try:
- phone = self._call_wechat(code)
- self._phone_cache[code] = phone
- return phone
- except Exception as e:
- logger.error(f"微信调用失败: {str(e)}")
- self._phone_cache[code] = '__FAIL__'
- return None
-
- # ---------- 微信接口调用(每次全新token) ----------
- def _call_wechat(self, code):
- token = self._get_new_token()
- url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
- resp = requests.post(url, json={'code': code}, timeout=10)
- data = resp.json()
- errcode = data.get('errcode')
- if errcode == 0:
- return data['phone_info']['purePhoneNumber']
- # 即使刚拿的token也可能意外失效,重试一次
- if errcode == 40001:
- token = self._get_new_token()
- url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
- resp = requests.post(url, json={'code': code}, timeout=10)
- data = resp.json()
- if data.get('errcode') == 0:
- return data['phone_info']['purePhoneNumber']
- raise Exception(f"微信错误(errcode={errcode}): {data.get('errmsg')}")
-
- # ---------- 获取全新 access_token(不缓存,每次调微信) ----------
- def _get_new_token(self):
- appid = getattr(settings, 'WEIXIN_APPID', '')
- secret = getattr(settings, 'WEIXIN_SECRET', '')
- if not appid or not secret:
- raise Exception('缺少微信配置')
- resp = requests.get('https://api.weixin.qq.com/cgi-bin/token', params={
- 'grant_type': 'client_credential',
- 'appid': appid,
- 'secret': secret
- })
- data = resp.json()
- token = data.get('access_token')
- if token:
- return token
- raise Exception(f"获取access_token失败: {data}")
-
- # ---------- 工具 ----------
- def _get_or_create_boss(self, user):
- try:
- return UserBoss.objects.get(user=user)
- except UserBoss.DoesNotExist:
- return UserBoss.objects.create(user=user, nickname='微信用户')
-
- def _upload_avatar(self, user, file):
- try:
- uid = user.yonghuid
- if not uid: return None
- old = user.avatar
- if old: delete_from_oss(old)
- ext = os.path.splitext(file.name)[1] or '.jpg'
- name = f"{uuid.uuid4().hex}{ext}"
- path = f"avatar/{uid}/{name}"
- return path if upload_to_oss(file, path) else None
- except:
- return None
-
-
-
-
-
-
-
-
-# yonghu/views.py 或 yonghu/api_views.py
-
-
-
-# yonghu/views.py 或 yonghu/api_views.py
-
-from django.utils import timezone
-from django.shortcuts import get_object_or_404
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework import status
-from .models import UserMain, UserDashou
-from shangpin.models import Huiyuangoumai, Gsfenhong # 需要导入会员模型
-import logging
-
-
-class DashouXinxiAPIView(APIView):
- """
- 获取打手信息接口
- 请求方式:POST
- 认证方式:JWT Token
- 返回:打手基本信息、资产信息、会员信息,金牌打手额外返回专属字段
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 获取当前登录用户
- current_user = request.user
- yonghuid = current_user.yonghuid
-
- # 2. 查询打手扩展表
- try:
- dashou_profile = current_user.dashou_profile
- except UserDashou.DoesNotExist:
- return Response({
- 'code': 400,
- 'message': '您不是打手身份',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 构建基础信息响应(原有字段完全保留)
- data = {
- 'dashounicheng': dashou_profile.nicheng or '',
- 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'dashouzhuangtai': dashou_profile.zhuangtai,
- 'yongjin': str(dashou_profile.yue) if dashou_profile.yue is not None else '0.00',
- 'zonge': str(dashou_profile.zonge) if dashou_profile.zonge is not None else '0.00',
- 'yajin': str(dashou_profile.yajin) if dashou_profile.yajin is not None else '0.00',
- 'jiedanZongliang':dashou_profile.jiedanzongliang if dashou_profile.jiedanzongliang is not None else '0',
- 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00',
- 'chengjiaoliang': dashou_profile.chengjiaozongliang,
- 'jifen': dashou_profile.jifen,
- 'chenghao': dashou_profile.chenghao or '',
- }
-
- # 4. 查询会员信息(原有逻辑)
- huiyuan_list = []
- try:
- huiyuan_records = Huiyuangoumai.objects.filter(
- yonghu_id=yonghuid
- ).select_related()
-
- for record in huiyuan_records:
- # 检查会员是否过期
- is_expired = record.jiance_shifou_daoqi()
-
- # 只返回未过期的会员(huiyuan_zhuangtai = 1)
- if record.huiyuan_zhuangtai == 1:
- huiyuan_list.append({
- 'huiyuanid': record.huiyuan_id,
- 'huiyuanming': record.jieshao or f"会员{record.huiyuan_id}",
- 'daoqi': record.daoqi_time
- })
- except Exception as e:
- # 会员信息查询失败不影响主要功能,返回空列表
- huiyuan_list = []
-
- data['clumber'] = huiyuan_list
-
- # 5. 如果是金牌打手,添加专属字段
- if dashou_profile.chenghao == "金牌打手":
- data.update({
- 'jiedanZongliang': dashou_profile.jiedanzongliang, # 接单总量
- 'jinriJiedan': str(dashou_profile.jinrishouyi) if dashou_profile.jinrishouyi is not None else '0.00', # 今日接单金额
- 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', # 最近提现金额(今日已提现)
- })
-
- # 6. 返回成功响应
- return Response({
- 'code': 200,
- 'message': '获取成功',
- 'data': data
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- # 生产环境应使用 logging 记录错误,而不是 print
- # logger.error(f"获取打手信息失败: {e}", exc_info=True)
- return Response({
- 'code': 500,
- 'message': f'服务器内部错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class HuoQuYaoQingRenView(APIView):
- """
- 获取当前打手的邀请人信息
- POST /yonghu/hqwdyqr
- Headers: Authorization: Bearer
- 返回邀请人的用户ID、昵称(来自老板扩展表)、头像相对URL
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- current_user = request.user # 当前登录用户
- yonghuid = current_user.yonghuid # 7位用户ID
-
- # 1. 校验打手身份及账号状态(只检查账号状态,不检查接单状态)
- try:
- dashou = UserDashou.objects.get(user=current_user)
- if dashou.zhanghaozhuangtai != 1:
- return Response({
- 'code': 403,
- 'message': '您的账号已被封禁,无法查看邀请人',
- 'data': None
- })
- except UserDashou.DoesNotExist:
- return Response({
- 'code': 404,
- 'message': '您还不是打手,无法查看邀请人',
- 'data': None
- })
-
- # 2. 获取邀请人ID
- yaoqingren_id = dashou.yaoqingren
- if not yaoqingren_id:
- return Response({
- 'code': 404,
- 'message': '您没有邀请人',
- 'data': None
- })
-
- # 3. 查询邀请人主表(头像)
- try:
- inviter_main = UserMain.objects.get(yonghuid=yaoqingren_id)
- touxiang = inviter_main.avatar or ''
- except UserMain.DoesNotExist:
- return Response({
- 'code': 404,
- 'message': '邀请人信息不存在',
- 'data': None
- })
-
- # 4. 查询老板扩展表(昵称),如果没有则使用空字符串
- nicheng = ''
- try:
- boss = UserBoss.objects.get(user=inviter_main)
- nicheng = boss.nickname or ''
- except UserBoss.DoesNotExist:
- pass # 邀请人可能不是老板,昵称留空
-
- response_data = {
- 'uid': yaoqingren_id,
- 'nicheng': nicheng,
- 'touxiang': touxiang # 相对路径,前端自行拼接
- }
-
- return Response({
- 'code': 200,
- 'message': '获取成功',
- 'data': response_data
- })
-
- except Exception as e:
- logger.error(f'获取邀请人失败: {str(e)}', exc_info=True)
- return Response({
- 'code': 500,
- 'message': '系统繁忙,请稍后重试',
- 'data': None
- })
-
-
-
-
-# yonghu/views.py 或 yonghu/api_views.py
-
-class ZaixianZhuangtaiAPIView(APIView):
- """
- 更改打手在线状态接口
- 请求方式:POST
- 认证方式:JWT Token
- 请求参数:status(0=离线,1=在线)
- 返回:更新后的状态信息
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 获取请求参数
- new_status = request.data.get('status')
-
- if new_status is None:
- return Response({
- 'code': 400,
- 'message': '缺少状态参数',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 转换为整数,确保是0或1
- try:
- new_status = int(new_status)
- if new_status not in [0, 1]:
- return Response({
- 'code': 400,
- 'message': '状态参数必须是0或1',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- except ValueError:
- return Response({
- 'code': 400,
- 'message': '状态参数必须是整数',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 获取当前登录用户
- current_user = request.user
- yonghuid = current_user.yonghuid
-
-
-
- # 3. 查询打手扩展表
- try:
- dashou_profile = current_user.dashou_profile
- except UserDashou.DoesNotExist:
-
- return Response({
- 'code': 400,
- 'message': '您不是打手身份',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 检查账号状态(是否被封禁)
- if dashou_profile.zhanghaozhuangtai != 1:
-
- return Response({
- 'code': 403,
- 'message': '账号状态异常,无法更改在线状态',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 5. 更新在线状态
- old_status = dashou_profile.zaixianzhuangtai
-
- # 如果状态没有变化,直接返回成功
- if old_status == new_status:
-
- return Response({
- 'code': 200,
- 'message': '状态未变化',
- 'data': {
- 'zaixianzhuangtai': new_status,
- 'status_text': '在线' if new_status == 1 else '离线'
- }
- }, status=status.HTTP_200_OK)
-
- # 更新状态
- dashou_profile.zaixianzhuangtai = new_status
- dashou_profile.save()
-
-
-
- # 6. 返回成功响应
- return Response({
- 'code': 200,
- 'message': '状态更新成功',
- 'data': {
- 'zaixianzhuangtai': new_status,
- 'status_text': '在线' if new_status == 1 else '离线'
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
-
- return Response({
- 'code': 500,
- 'message': f'服务器内部错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-from shangpin.models import Gsfenhong # 需要导入会员模型
-class GuanshiXinxiView(APIView):
- """
- 获取管事信息接口
- 路径:/yonghu/guanshixinxi
- 方法:POST
- 权限:JWT认证
- 功能:获取当前管事的详细信息,包括基本信息、分红信息以及已充值打手数量
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 获取当前用户(通过JWT认证)
- current_user = request.user
-
- # 查询管事扩展表
- try:
- guanshi_profile = UserGuanshi.objects.get(user=current_user)
-
- # 🔥【新增】查询已充值打手数量(从分红表中统计)
- # Gsfenhong 表中 guanshi 字段存储管事ID(yonghuid)
- yichongzhi_count = Gsfenhong.objects.filter(guanshi=current_user.yonghuid).count()
-
- # 返回管事信息
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': {
- 'gszhstatus': guanshi_profile.zhuangtai, # 管事账号状态
- 'yaoqingzongshu': guanshi_profile.yaogingshuliang, # 邀请打手总数
- 'fenyongzonge': float(guanshi_profile.chongzhifenrun), # 充值分佣总额
- 'fenyongtixian': float(guanshi_profile.yue), # 可提现余额
- 'yichongzhiDashou': yichongzhi_count, # 已充值打手数量
- }
- })
-
- except UserGuanshi.DoesNotExist:
- # 用户不是管事,返回错误
- return Response({
- 'code': 400,
- 'msg': '您还不是管事,请先注册',
- 'data': None
- }, status=400)
-
- except Exception as e:
- # 捕获所有异常,返回错误信息并记录日志
- import logging
- logger = logging.getLogger(__name__)
- logger.error(f"获取管事信息异常: {str(e)}", exc_info=True)
- return Response({
- 'code': 500,
- 'msg': f'获取信息失败: {str(e)}',
- 'data': None
- }, status=500)
-
-
-
-
-# yonghu/views.py
-
-from rest_framework import status
-from django.core.exceptions import ObjectDoesNotExist
-
-from django.db.models import Sum
-from houtai.models import ShangjiaRiTongji # 商家日统计表
-class ShangJiaXinXiView(APIView):
- """
- 获取商家信息接口 (POST /yonghu/shangjiaxinxi)
- 权限:需JWT认证
- 返回:商家基本信息 + 称号列表 + 今日/本月统计
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 获取商家扩展资料
- shangjia = request.user.shop_profile
- except ObjectDoesNotExist:
- return Response({
- 'code': 4001,
- 'msg': '您还未注册成为商家或商家资料不存在',
- 'data': None
- }, status=status.HTTP_200_OK)
-
- user = request.user
- yonghuid = user.yonghuid
- today = timezone.now().date()
- this_month_start = today.replace(day=1)
-
- # ------------------- 1. 从 UserShangjia 获取基础字段 -------------------
- response_data = {
- 'sjzhzhuangtai': shangjia.zhuangtai, # 商家账号状态
- 'fadanzong': shangjia.fabu, # 发单总量(累计)
- 'tuikuanzong': shangjia.tuikuan, # 退款总量(累计)
- 'sjyue': float(shangjia.yue), # 商家余额
- # 'riliushui' 和 'yueliushui' 将从统计表计算,不再使用原字段
- }
-
- # ------------------- 2. 获取称号列表(仅限商家称号) -------------------
- chenghao_list = []
- # 查询当前用户的所有称号关联
- yonghu_chenghao_qs = YonghuChenghao.objects.filter(yonghu=user).select_related('chenghao')
- for yc in yonghu_chenghao_qs:
- chenghao_obj = yc.chenghao
- # 只保留类型为“商家”的称号
- if chenghao_obj.leixing == 'shangjia':
- chenghao_list.append({
- 'mingcheng': chenghao_obj.mingcheng,
- 'texiao_json': chenghao_obj.texiao_miaoshu, # 前端需要的特效描述
- # 如果还需要其他字段(如图标URL)可在此添加,打手端有 texiao_json
- })
- response_data['chenghao_list'] = chenghao_list
-
- # ------------------- 3. 从 ShangjiaRiTongji 获取今日统计 -------------------
- try:
- today_stat = ShangjiaRiTongji.objects.get(yonghuid=yonghuid, riqi=today)
- jinri_paifa_dingdan = today_stat.paifa_dingdan_shu
- jinri_paifa_jine = float(today_stat.paifa_jine)
- jinri_tuikuan_dingdan = today_stat.tuikuan_dingdan_shu
- except ShangjiaRiTongji.DoesNotExist:
- jinri_paifa_dingdan = 0
- jinri_paifa_jine = 0.00
- jinri_tuikuan_dingdan = 0
-
- response_data['jinripaidan'] = jinri_paifa_dingdan # 今日派单数(新增)
- response_data['jinrituikuan'] = jinri_tuikuan_dingdan # 今日退款数(新增)
- response_data['riliushui'] = jinri_paifa_jine # 今日流水(派发总金额)
-
- # ------------------- 4. 从 ShangjiaRiTongji 获取本月统计 -------------------
- month_stats = ShangjiaRiTongji.objects.filter(
- yonghuid=yonghuid,
- riqi__gte=this_month_start,
- riqi__lte=today
- ).aggregate(
- total_paifa_jine=Sum('paifa_jine'),
- total_paifa_dingdan=Sum('paifa_dingdan_shu')
- )
- yue_paifa_jine = float(month_stats.get('total_paifa_jine') or 0.00)
- # yue_paifa_dingdan = month_stats.get('total_paifa_dingdan') or 0 # 如果前端不需要可不加
-
- response_data['yueliushui'] = yue_paifa_jine # 今月流水(派发总金额)
-
- # (可选)如果前端还需要今月派单总数,取消下面注释
- # response_data['jinyuepaidan'] = yue_paifa_dingdan
-
- return Response({
- 'code': 200,
- 'msg': '获取商家信息成功',
- 'data': response_data
- }, status=status.HTTP_200_OK)
-
-
-
-
-
-
-
-
-
-
-
-
-class BossBiaoshiView(APIView):
- """
- 获取用户的GoEasy标识 - 极简实现
- POST /yonghu/bossbiaoshi
- 需要JWT Token认证
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 直接从request.user获取用户
- user = request.user
-
- # 2. 获取yonghuid(确保用户已登录)
- if not user or not hasattr(user, 'yonghuid'):
- return Response({
- 'code': 401,
- 'msg': '用户未认证或用户信息不完整'
- }, status=401)
-
- # 3. 生成标识:Boss + yonghuid(与前端约定)
- boss_biaoshi = f"Boss{user.yonghuid}"
-
- # 4. 返回给前端
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': {
- 'boss_biaoshi': boss_biaoshi,
- 'yonghuid': user.yonghuid,
- 'user_type': user.user_type
- }
- })
-
-
-"""
-阿龙电竞陪玩平台 - 邀请码相关API接口
-"""
-
-from utils.yaoqingma_utils import chuangjianYaoqingma, yanzhengYaoqingmaWeiyixing
-
-
-class YaoqingmaView(APIView):
- """
- 获取或生成管事邀请码
- 访问路径: /api/yonghu/yaoqingma/
- 请求方法: GET
- 权限要求: JWT Token认证通过的用户
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- """
- 获取当前用户的邀请码
- 逻辑:
- 1. 通过JWT获取当前用户 (request.user)
- 2. 反向查询UserGuanshi扩展表
- 3. 验证管事状态(zhuangtai=1)
- 4. 已有邀请码直接返回,没有则生成并保存
- """
- # 获取当前认证用户
- current_user = request.user
-
- # 1. 使用反向查询获取管事扩展信息 (性能最优方式)
- try:
- guanshi_profile = current_user.guanshi_profile
- except UserGuanshi.DoesNotExist:
- # 用户不是管事,没有扩展表记录
- return Response({
- 'code': 403,
- 'message': '当前用户不是管事身份,无法生成邀请码',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 2. 验证管事状态 (zhuangtai 必须等于 1)
- if guanshi_profile.zhuangtai != 1:
- return Response({
- 'code': 403,
- 'message': '管事账号状态异常,无法生成邀请码',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 3. 检查是否已存在邀请码
- existing_yaoqingma = guanshi_profile.yaoqingma
-
- # 如果存在且非空,直接返回
- if existing_yaoqingma and existing_yaoqingma.strip() != '':
- return Response({
- 'code': 200,
- 'message': '成功获取现有邀请码',
- 'data': {
- 'yaoqingma': existing_yaoqingma,
- 'is_new': False # 标记是否为新生成的
- }
- })
-
- # 4. 生成新的邀请码 (使用用户ID字符串)
- # 注意: 这里使用yonghuid,不是主键id,如你要求的"123456"这种形式
- yonghuid_str = str(current_user.yonghuid)
- new_yaoqingma = chuangjianYaoqingma(yonghuid_str)
-
- # 5. 验证生成的邀请码格式
- if not yanzhengYaoqingmaWeiyixing(new_yaoqingma):
- return Response({
- 'code': 500,
- 'message': '邀请码生成失败,格式验证错误',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 6. 保存到数据库 (使用事务确保数据一致性)
- try:
- with transaction.atomic():
- guanshi_profile.yaoqingma = new_yaoqingma
- guanshi_profile.save()
- except Exception as e:
- # 这里可以记录日志
- return Response({
- 'code': 500,
- 'message': f'保存邀请码到数据库失败: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 7. 返回新生成的邀请码
- return Response({
- 'code': 200,
- 'message': '成功生成新的邀请码',
- 'data': {
- 'yaoqingma': new_yaoqingma,
- 'is_new': True # 标记是新生成的
- }
- })
-
-
-
-"""
-阿龙电竞陪玩平台 - 打手注册及相关API接口
-"""
-
-from shangpin.models import Huiyuangoumai # 导入会员购买模型
-from utils.yaoqingma_utils import chuangjianYaoqingma # 导入之前写的邀请码生成算法
-
-class DashouZhuceView(APIView):
- """
- 打手注册接口 (通过管事邀请码)
- 访问路径: /api/yonghu/dashouzhuce/
- 请求方法: POST
- 权限要求: JWT Token认证通过的用户
- 请求体: { "inviteCode": "AbC123..." }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 获取并验证邀请码参数
- yaoqingma = request.data.get('inviteCode')
- if not yaoqingma:
- return Response({
- 'code': 400,
- 'message': '邀请码不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- yaoqingma = yaoqingma.strip()
- if len(yaoqingma) > 100:
- return Response({
- 'code': 400,
- 'message': '邀请码长度超过限制',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- current_user = request.user
- # 注意:这里request.user是UserMain实例
-
- # 2. 检查用户是否已是打手 (使用反向查询,性能最优)
- try:
- dashou_profile = current_user.dashou_profile
- # 用户已是打手,直接返回现有信息
- return self.fanhuiXianyouDashouXinxi(dashou_profile, current_user)
- except UserDashou.DoesNotExist:
- # 用户不是打手,继续注册流程
- pass
-
- # 3. 根据邀请码查找对应的管事
- try:
- # 使用select_related一次性获取管事及其关联的主表用户信息,性能最高
- guanshi_profile = UserGuanshi.objects.select_related('user').get(yaoqingma=yaoqingma)
- except UserGuanshi.DoesNotExist:
- return Response({
- 'code': 404,
- 'message': '邀请码无效或不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 验证管事状态
-
- # 5. 核心:在数据库事务中创建用户的所有身份
- try:
- with transaction.atomic():
- # 获取管事的用户ID (yonghuid)
- guanshi_yonghuid = guanshi_profile.user.yonghuid
-
- # 5.1 创建打手扩展表
- dashou_profile = UserDashou.objects.create(
- user=current_user,
- nicheng='大手子', # 默认昵称
- chenghao='普通大手', # 默认称号
- yaoqingren=guanshi_yonghuid, # 设置邀请人ID
- # 其他字段全部使用模型定义的默认值
- )
-
- # 5.2 创建商家扩展表
- try:
- boss_profile = current_user.boss_profile
- if boss_profile.nickname:
- boss_nickname = boss_profile.nickname
- except:
- pass
-
-
- # 5.3 创建管事扩展表
-
- # 5.4 更新主表的用户类型 (如果需要,可以设为'dashou',或保持原逻辑)
- # current_user.user_type = 'dashou' # 根据你的业务逻辑决定
- # current_user.save()
-
- # 5.5 更新原管事的邀请数量 (使用行锁防止并发问题)
- # 重新查询并锁定该行记录
- locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
- locked_guanshi.yaogingshuliang += 1
- locked_guanshi.save()
-
- # ========== 新增:管事每日统计(邀请打手) ==========
- try:
- import logging
-
- logger = logging.getLogger(__name__)
- from houtai.utils import update_guanshi_daily_by_action
- update_guanshi_daily_by_action(
- yonghuid=guanshi_yonghuid,
- action=1, # 1 = 邀请打手
- amount=Decimal('0.00')
- )
- logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}")
- except Exception as e:
- logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}")
-
- except Exception as e:
-
- # 事务会自动回滚
- # 这里可以记录更详细的日志
- return Response({
- 'code': 500,
- 'message': f'注册过程中发生系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 6. 注册成功,返回信息 (新注册用户没有会员记录,clumber返回空列表)
- fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=True)
- return Response({
- 'code': 200,
- 'message': '注册成功!已开通打手、管事身份。',
- 'data': fanhui_data
- })
-
- def fanhuiXianyouDashouXinxi(self, dashou_profile, current_user):
- """处理已是打手的用户,返回其现有信息"""
- fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=False)
- return Response({
- 'code': 200,
- 'message': '您已是打手,返回当前信息。',
- 'data': fanhui_data
- })
-
- def zhuangbeiFanhuiShuju(self, dashou_profile, current_user, is_new=False):
- """准备返回给前端的打手数据,字段名严格匹配前端"""
-
-
-
- # 构建基础信息
- data = {
- 'dashounicheng': dashou_profile.nicheng,
- 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
- 'yongjin': str(dashou_profile.yue), # Decimal转字符串
- 'zonge': str(dashou_profile.zonge),
- 'yajin': str(dashou_profile.yajin),
- 'chenghao': dashou_profile.chenghao,
- 'jinfen': dashou_profile.jifen,
- 'chengjiaoliang': dashou_profile.chengjiaozongliang,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'dashouzhuangtai': dashou_profile.zhuangtai,
- }
-
- # 查询并处理会员列表 (clumber)
- # 使用当前用户的yonghuid进行查询
- huiyuan_goumai_list = []
- if not is_new: # 如果是新用户,直接返回空列表,无需查询
- goumai_records = Huiyuangoumai.objects.filter(yonghu_id=current_user.yonghuid)
- for record in goumai_records:
- # 调用方法检查是否到期,此方法内部会更新状态
- shifou_daoqi = record.jiance_shifou_daoqi()
- huiyuan_goumai_list.append({
- 'huiyuan_id': record.huiyuan_id,
- 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
- 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
- 'shifou_daoqi': shifou_daoqi,
- 'jieshao': record.jieshao
- })
-
- data['clumber'] = huiyuan_goumai_list
- return data
-
-
-
-
-from django.core.paginator import Paginator, EmptyPage
-
-
-
-
-class GuanshiYaoqingDashouListView(APIView):
- """
- 管事获取已邀请打手列表(支持筛选、搜索、分页)
- POST /api/yonghu/gshqyqds/
- Headers: Authorization: Bearer
- Body:
- {
- "page": 1, // 页码,从1开始
- "pageSize": 30, // 每页条数,最大100
- "keyword": "", // 搜索关键词(用户ID或昵称)
- "zhanghaozhuangtai": 1, // 账号状态:1=正常,2=封禁(可选)
- "zaixianzhuangtai": 1 // 在线状态:1=在线,2=离线(可选)
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- current_user = request.user
- guanshi_yonghuid = current_user.yonghuid
-
- # 1. 分页参数
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 30))
- if page < 1 or page_size < 1:
- return Response({'code': 400, 'message': '分页参数必须大于0'}, status=400)
- if page_size > 100:
- page_size = 100
-
- # 2. 筛选参数
- keyword = request.data.get('keyword', '').strip()
- zhanghaozhuangtai = request.data.get('zhanghaozhuangtai') # 可选:1=正常,2=封禁
- zaixianzhuangtai = request.data.get('zaixianzhuangtai') # 可选:1=在线,2=离线
-
- # 3. 基础查询:当前管事邀请的所有打手
- base_qs = UserDashou.objects.select_related('user').filter(
- yaoqingren=guanshi_yonghuid
- )
-
- # 4. 全局统计(不受筛选影响,直接基于 base_qs)
- total_all = base_qs.count()
- zaixian_all = base_qs.filter(zaixianzhuangtai=1).count()
- zhengchang_all = base_qs.filter(zhanghaozhuangtai=1).count()
- fengjin_all = total_all - zhengchang_all
-
- # 5. 应用筛选条件
- if keyword:
- base_qs = base_qs.filter(
- Q(nicheng__icontains=keyword) | Q(user__yonghuid__icontains=keyword)
- )
- if zhanghaozhuangtai is not None:
- try:
- val = int(zhanghaozhuangtai)
- if val == 1:
- base_qs = base_qs.filter(zhanghaozhuangtai=1)
- else:
- base_qs = base_qs.exclude(zhanghaozhuangtai=1)
- except (ValueError, TypeError):
- pass
- if zaixianzhuangtai is not None:
- try:
- val = int(zaixianzhuangtai)
- if val == 1:
- base_qs = base_qs.filter(zaixianzhuangtai=1)
- else:
- base_qs = base_qs.exclude(zaixianzhuangtai=1)
- except (ValueError, TypeError):
- pass
-
- # 6. 排序和分页
- base_qs = base_qs.order_by('-create_time')
- paginator = Paginator(base_qs, page_size)
- try:
- page_obj = paginator.page(page)
- except EmptyPage:
- page_obj = []
- has_more = False
- filtered_total = paginator.count if paginator.count else 0
- else:
- has_more = page_obj.has_next()
- filtered_total = paginator.count
-
- # 7. 构建打手列表
- dashou_list = []
- for dashou in page_obj:
- user_main = dashou.user
- dashou_list.append({
- 'uid': user_main.yonghuid,
- 'nicheng': dashou.nicheng or '打手',
- 'zaixianzhuangtai': dashou.zaixianzhuangtai,
- 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
- 'touxiang': user_main.avatar or '',
- 'jiedanzongliang': dashou.jiedanzongliang,
- 'chengjiaozongliang': dashou.chengjiaozongliang,
- 'tuikuanliang': dashou.tuikuanliang,
- 'yue': str(dashou.zonge),
- })
-
- # 8. 获取管事邀请总数(来自管事扩展表)
- try:
- yaoqingzongshu = current_user.guanshi_profile.yaogingshuliang
- except Exception:
- yaoqingzongshu = total_all
-
- response_data = {
- 'dashouList': dashou_list,
- 'yaoqingzongshu': yaoqingzongshu, # 邀请总数(全局)
- 'totalCount': filtered_total, # 当前筛选条件下的总数
- 'zaixianCount': zaixian_all, # 全局在线
- 'zhengchangCount': zhengchang_all, # 全局正常
- 'fengjinCount': fengjin_all, # 全局封禁
- 'currentPage': page,
- 'pageSize': page_size,
- 'hasMore': has_more,
- }
-
- return Response({'code': 200, 'message': '获取成功', 'data': response_data})
-
- except ValueError:
- return Response({'code': 400, 'message': '分页参数格式错误'}, status=400)
- except Exception as e:
- logger.error(f'获取打手列表失败: {str(e)}', exc_info=True)
- return Response({'code': 500, 'message': '系统错误'}, status=500)
-
-
-
-from rest_framework.views import APIView
-from rest_framework import permissions, status
-from rest_framework.response import Response
-from django.db import transaction
-from .models import UserMain, UserDashou, UserGuanshi, Tixianjilu
-import os
-import uuid
-from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
-from dingdan.models import Dingdan, DingdanPingtai, Lilubiao
-
-
-
-class TixianXinxiHuoquView(APIView):
- """
- 获取用户提现收款信息接口
- 返回用户已设置的手机号、收款码、收款账号
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- """
- 处理获取收款信息的POST请求
- 直接返回用户当前的收款设置
- """
- try:
- # 从request.user获取当前用户
- user_main = request.user
-
- # 🔥【修改开始 - 使用Decimal,字段名是lilu】
- # 查询打手提现费率 (fadanpingtai='5')
- dashou_rate_obj = Lilubiao.objects.filter(fadanpingtai='5').first()
- dashou_rate = decimal.Decimal(str(dashou_rate_obj.lilu)) if dashou_rate_obj else decimal.Decimal('0.00')
-
- # 查询管事提现费率 (fadanpingtai='6')
- guanshi_rate_obj = Lilubiao.objects.filter(fadanpingtai='6').first()
- guanshi_rate = decimal.Decimal(str(guanshi_rate_obj.lilu)) if guanshi_rate_obj else decimal.Decimal('0.00')
- # 🔥【修改结束】
-
- # 构建返回数据
- response_data = {
- 'txdianhua': user_main.phone if user_main.phone else '',
- 'txtupian': user_main.zhifu if user_main.zhifu else '',
- 'txzh': user_main.skzhanghao if user_main.skzhanghao else '',
- # 🔥【修改开始】
- 'dashou_rate': dashou_rate, # 打手提现费率
- 'guanshi_rate': guanshi_rate, # 管事提现费率
- # 🔥【修改结束】
- }
-
- # 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': response_data
- })
-
- except Exception as e:
- return Response({
- 'code': 99,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-from rest_framework.parsers import MultiPartParser, FormParser,JSONParser
-
-
-class ShoukuanXinxiShangchuanView(APIView):
- """
- 上传用户收款信息接口
- 支持同时上传手机号、收款账号、收款码图片
- 使用multipart/form-data格式
- """
- permission_classes = [permissions.IsAuthenticated]
- parser_classes = (MultiPartParser,FormParser,JSONParser)
-
- def post(self, request):
- """
- 处理上传收款信息的POST请求
- 验证并保存用户的收款设置
- """
- try:
- # 获取当前用户
- user_main = request.user
- yonghuid = user_main.yonghuid
-
- # 准备响应数据
- response_data = {}
-
- # 1. 验证必填字段:手机号必须要有
- txdianhua = request.data.get('txdianhua', '').strip()
- if not txdianhua:
- return Response({
- 'code': 1,
- 'msg': '收款人电话不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证手机号格式(11位数字)
- if not txdianhua.isdigit() or len(txdianhua) != 11:
- return Response({
- 'code': 2,
- 'msg': '请输入11位有效的手机号码',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 获取收款账号(可能为空)
- txzh = request.data.get('txzh', '').strip()
-
- # 3. 处理收款码图片上传(如果有)
- txtupian_file = request.FILES.get('file') # 注意:前端upload.js使用'file'作为字段名
- txtupian_url = ''
-
- if txtupian_file:
- # 验证图片
- is_valid, error_msg = validate_image(txtupian_file)
- if not is_valid:
- return Response({
- 'code': 3,
- 'msg': f'图片验证失败: {error_msg}',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 上传收款码图片
- txtupian_url = self.handle_shoukuanma_upload(user_main, txtupian_file)
- if txtupian_url:
- response_data['txtupian'] = txtupian_url
- else:
- return Response({
- 'code': 4,
- 'msg': '收款码上传失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
- else:
- response_data['txtupian'] = ''
-
- # 4. 验证收款账号和收款码至少有一个
- if not txzh and not txtupian_url:
- return Response({
- 'code': 5,
- 'msg': '请填写收款账号或上传收款码',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 保存所有信息到数据库
- with transaction.atomic():
- user_main.phone = txdianhua
- user_main.skzhanghao = txzh if txzh else ''
-
- # 只有上传了图片才更新zhifu字段
- if txtupian_url:
- user_main.zhifu = txtupian_url
-
- user_main.save()
-
- # 6. 构建完整的返回数据
- response_data['txdianhua'] = txdianhua
- response_data['txzh'] = txzh if txzh else ''
-
- # 7. 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '收款信息设置成功',
- 'data': response_data
- })
-
- except Exception as e:
- import traceback
- traceback.print_exc() # 打印详细错误信息到控制台
- return Response({
- 'code': 99,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def handle_shoukuanma_upload(self, user_main, image_file):
- """
- 处理收款码图片上传到腾讯云COS
- 存储路径: tixian/{yonghuid}/{yonghuid}.扩展名
- """
- try:
- yonghuid = user_main.yonghuid
- if not yonghuid:
- return None
-
- # 获取旧收款码URL
- old_shoukuanma_url = user_main.zhifu
-
- # 生成文件扩展名
- file_name = image_file.name
- file_extension = os.path.splitext(file_name)[1].lower()
- if not file_extension:
- file_extension = '.jpg'
-
- # 构建OSS文件路径: tixian/{yonghuid}/{yonghuid}{扩展名}
- # 使用用户ID作为文件名,确保同一用户多次上传会覆盖
- oss_file_name = f"{yonghuid}{file_extension}"
- oss_file_path = f"tixian/{yonghuid}/{oss_file_name}"
-
- # 删除旧收款码(如果存在)
- if old_shoukuanma_url and old_shoukuanma_url.strip():
- delete_from_oss(old_shoukuanma_url)
-
- # 上传新收款码到OSS
- new_shoukuanma_url = upload_to_oss(image_file, oss_file_path)
-
- if new_shoukuanma_url:
- # 返回相对路径(从tixian文件夹开始)
- return oss_file_path
- else:
- return None
-
- except Exception as e:
- print(f"处理收款码上传失败: {e}")
- return None
-
-
-
-
-
-
-
-from rest_framework.views import APIView
-from rest_framework import permissions, status
-from rest_framework.response import Response
-from django.db import transaction
-from django.db.models import F
-import decimal
-import traceback
-from .models import UserMain, UserDashou, UserGuanshi, Tixianjilu
-
-
-
-
-
-
-
-
-from utils.fadan_utils import check_fadan_qiangdan_eligible
-
-
-# ========== 订单状态常量(请根据您的实际业务修改)==========
-# 假设状态值:已完成=9,已退款=10(示例,实际以您的项目为准)
-ORDER_STATUS_COMPLETED = 3 # 订单已完成
-ORDER_STATUS_REFUNDED = 5 # 订单已退款
-ORDER_STATUS_FAIL = 6
-# 进行中状态(不可提现)例如:待接单、已接单、进行中等,您需要定义排除列表
-FORBIDDEN_STATUS_FOR_WITHDRAW = [1, 2, 3, 4, 5, 6, 7, 8] # 除已完成和已退款外的所有状态
-
-
-class TixianShenqingView(APIView):
- """
- 用户提现申请接口(完整版)
- 支持类型:
- 1-打手佣金提现
- 2-管事分红提现
- 3-组长分红提现
- 4-审核官分佣提现
- 5-打手押金提现(退出平台)
- 6-商家余额提现(退出平台)
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- user_main = request.user
- yonghuid = user_main.yonghuid
-
- # 1. 获取并验证参数
- leixing = request.data.get('leixing')
- jine = request.data.get('jine')
- fangshi = request.data.get('fangshi')
- txskfs = request.data.get('txskfs', 0)
-
- # 罚单检查(共用)
- fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2)
- if not fadan_ok:
- return Response({'code': 400, 'msg': fadan_msg})
-
- # 参数完整性
- missing = []
- if leixing is None: missing.append('leixing')
- if jine is None: missing.append('jine')
- if fangshi is None: missing.append('fangshi')
- if missing:
- return Response({'code': 1, 'msg': f'缺少参数: {", ".join(missing)}'},
- status=status.HTTP_400_BAD_REQUEST)
-
- try:
- leixing = int(leixing)
- jine = decimal.Decimal(str(jine))
- fangshi = int(fangshi)
- txskfs = int(txskfs)
- except (ValueError, TypeError, decimal.InvalidOperation) as e:
- return Response({'code': 2, 'msg': f'参数格式错误: {str(e)}'},
- status=status.HTTP_400_BAD_REQUEST)
-
- if jine <= decimal.Decimal('0'):
- return Response({'code': 3, 'msg': '提现金额必须大于0'},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 支持的提现类型(1-6)
- if leixing not in [1, 2, 3, 4, 5, 6]:
- return Response({'code': 4, 'msg': '无效的提现类型'},
- status=status.HTTP_400_BAD_REQUEST)
-
- if fangshi not in [1, 2]:
- return Response({'code': 5, 'msg': '无效的收款方式,只能是1(微信)或2(支付宝)'},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 获取对应费率及手续费
- current_rate = decimal.Decimal('0.00')
- rate_key = {
- 1: '5', # 打手佣金
- 2: '6', # 管事分红
- 3: '8', # 组长分红
- 4: '9', # 审核官分佣
- 5: '11', # 打手押金提现
- 6: '10', # 商家余额提现
- }.get(leixing)
-
- if rate_key:
- rate_obj = Lilubiao.objects.filter(fadanpingtai=rate_key).first()
- if rate_obj:
- current_rate = decimal.Decimal(str(rate_obj.lilu))
- else:
- return Response({'code': 41, 'msg': f'提现费率未配置(类型{leixing})'},
- status=status.HTTP_400_BAD_REQUEST)
-
- shouxufei = jine * current_rate
- shijidaozhang = jine - shouxufei
- if shijidaozhang <= decimal.Decimal('0'):
- return Response({'code': 6, 'msg': '提现金额过低,扣除手续费后无实际到账'},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 根据提现类型执行校验和扣款
- nicheng = ''
- if leixing == 1:
- result = self._check_dashou_commission_withdraw(user_main, jine)
- if result['code'] != 0:
- return Response(result, status=status.HTTP_400_BAD_REQUEST)
- nicheng = result['data']['nicheng']
- # 扣款(佣金)
- with transaction.atomic():
- profile = UserDashou.objects.select_for_update().get(user=user_main)
- if profile.yue < jine:
- return Response({'code': 13, 'msg': '余额不足'})
- profile.yue = F('yue') - jine
- profile.save()
- elif leixing == 2:
- result = self._check_guanshi_dividend_withdraw(user_main, jine)
- if result['code'] != 0:
- return Response(result, status=status.HTTP_400_BAD_REQUEST)
- with transaction.atomic():
- profile = UserGuanshi.objects.select_for_update().get(user=user_main)
- if profile.yue < jine:
- return Response({'code': 22, 'msg': '余额不足'})
- profile.yue = F('yue') - jine
- profile.save()
- nicheng = "管事用户"
- elif leixing == 3:
- result = self._check_zuzhang_withdraw(user_main, jine)
- if result['code'] != 0:
- return Response(result, status=status.HTTP_400_BAD_REQUEST)
- with transaction.atomic():
- profile = UserZuzhang.objects.select_for_update().get(user=user_main)
- if profile.ketixian_jine < jine:
- return Response({'code': 32, 'msg': '可提现金额不足'})
- profile.ketixian_jine = F('ketixian_jine') - jine
- profile.save()
- nicheng = user_main.nicheng if hasattr(user_main, 'nicheng') else ''
- elif leixing == 4:
- result = self._check_shenheguan_withdraw(user_main, jine)
- if result['code'] != 0:
- return Response(result, status=status.HTTP_400_BAD_REQUEST)
- with transaction.atomic():
- profile = UserShenheguan.objects.select_for_update().get(user=user_main)
- if profile.yue < jine:
- return Response({'code': 42, 'msg': '可提现金额不足'})
- profile.yue = F('yue') - jine
- profile.save()
- nicheng = "审核官"
- elif leixing == 5:
- # 打手押金提现(退出)
- result = self._check_dashou_yajin_withdraw(user_main, jine)
- if result['code'] != 0:
- return Response(result, status=status.HTTP_400_BAD_REQUEST)
- with transaction.atomic():
- profile = UserDashou.objects.select_for_update().get(user=user_main)
- if profile.yajin < jine:
- return Response({'code': 53, 'msg': '押金余额不足'})
- profile.yajin = F('yajin') - jine
- # 押金提现后封禁打手账号
- profile.zhanghaozhuangtai = 0 # 0表示禁用
- profile.save()
- nicheng = profile.nicheng or ''
- elif leixing == 6:
- # 商家余额提现(退出)
- result = self._check_shangjia_withdraw(user_main, jine)
- if result['code'] != 0:
- return Response(result, status=status.HTTP_400_BAD_REQUEST)
- with transaction.atomic():
- profile = UserShangjia.objects.select_for_update().get(user=user_main)
- if profile.yue < jine:
- return Response({'code': 63, 'msg': '商家余额不足'})
- profile.yue = F('yue') - jine
- # 注意:是否封禁商家账号?根据需求,用户未明确要求封禁,暂时只扣款,可后续扩展。
- profile.save()
- nicheng = profile.nicheng or ''
-
- # 4. 创建提现记录(先扣款后创建,确保资金安全)
- try:
- avatar = user_main.avatar or ''
- phone = user_main.phone or ''
- zhifu = user_main.zhifu or ''
- skzhanghao = user_main.skzhanghao or ''
- tixian_record = Tixianjilu.objects.create(
- yonghuid=yonghuid,
- avatar=avatar,
- phone=phone,
- nicheng=nicheng,
- leixing=leixing,
- zhifu=zhifu,
- skzhanghao=skzhanghao,
- jine=shijidaozhang, # 记录实际到账金额
- zhuangtai=1, # 审核中
- fangshi=fangshi,
- shenheid=None,
- bhliyou='',
- )
- except Exception as e:
- return Response({'code': 99, 'msg': f'创建提现记录失败: {str(e)}'},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 5. 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '提现申请提交成功',
- 'data': {
- 'tixian_id': tixian_record.id,
- 'leixing': leixing,
- 'jine': str(jine.quantize(decimal.Decimal('0.01'))),
- 'fangshi': fangshi,
- 'zhuangtai': 1,
- 'create_time': tixian_record.create_time.strftime('%Y-%m-%d %H:%M:%S'),
- 'current_rate': str(current_rate.quantize(decimal.Decimal('0.0000'))),
- 'shouxufei': str(shouxufei.quantize(decimal.Decimal('0.01'))),
- 'shijidaozhang': str(shijidaozhang.quantize(decimal.Decimal('0.01'))),
- 'tip': f'本次提现手续费率{current_rate * 100}%,手续费{shouxufei}元,实际到账{shijidaozhang}元'
- }
- })
-
- except Exception as e:
- traceback.print_exc()
- return Response({'code': 99, 'msg': f'系统错误: {str(e)}'},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # ==================== 校验函数 ====================
-
- def _check_dashou_commission_withdraw(self, user_main, jine):
- """打手佣金提现校验"""
- try:
- dashou = UserDashou.objects.get(user=user_main)
- except ObjectDoesNotExist:
- return {'code': 10, 'msg': '您不是打手身份,无法提现佣金'}
-
- if dashou.zhanghaozhuangtai != 1:
- return {'code': 11, 'msg': '打手账号已被封禁,无法提现'}
- if dashou.zhuangtai != 1:
- return {'code': 12, 'msg': '您有订单进行中,请完成后提现'}
- if dashou.jifen != 10:
- return {'code': 15, 'msg': '积分不足10分,请补充积分后提现'}
- if dashou.yue < jine:
- return {'code': 13, 'msg': f'余额不足,当前余额: {dashou.yue}'}
-
- return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
-
- def _check_guanshi_dividend_withdraw(self, user_main, jine):
- """管事分红提现校验"""
- try:
- guanshi = UserGuanshi.objects.get(user=user_main)
- except ObjectDoesNotExist:
- return {'code': 20, 'msg': '您不是管事身份,无法提现分红'}
- if guanshi.zhuangtai != 1:
- return {'code': 21, 'msg': '管事账号已被封禁,无法提现'}
- if guanshi.yue < jine:
- return {'code': 22, 'msg': f'余额不足,当前余额: {guanshi.yue}'}
- return {'code': 0, 'msg': 'ok', 'data': {}}
-
- def _check_zuzhang_withdraw(self, user_main, jine):
- """组长分红提现校验"""
- try:
- zuzhang = UserZuzhang.objects.get(user=user_main)
- except ObjectDoesNotExist:
- return {'code': 30, 'msg': '您不是组长身份,无法提现组长分红'}
- if zuzhang.zhuangtai != 1:
- return {'code': 31, 'msg': '组长账号已被封禁,无法提现'}
- if zuzhang.ketixian_jine < jine:
- return {'code': 32, 'msg': f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}'}
- return {'code': 0, 'msg': 'ok', 'data': {}}
-
- def _check_shenheguan_withdraw(self, user_main, jine):
- """审核官分佣提现校验"""
- try:
- shenheguan = UserShenheguan.objects.get(user=user_main)
- except ObjectDoesNotExist:
- return {'code': 40, 'msg': '您不是审核官身份,无法提现分佣'}
- if shenheguan.zhuangtai != 1:
- return {'code': 41, 'msg': '审核官账号已被封禁,无法提现'}
- if shenheguan.yue < jine:
- return {'code': 42, 'msg': f'可提现金额不足,当前可提: {shenheguan.yue}'}
- return {'code': 0, 'msg': 'ok', 'data': {}}
-
- from django.db.models import Q
-
- def _check_shangjia_withdraw(self, user_main, jine):
- """
- 商家余额提现校验(退出平台)
- 包含:订单、自身罚单、申请的罚单检查
- """
- try:
- shangjia = UserShangjia.objects.get(user=user_main)
- except ObjectDoesNotExist:
- return {'code': 60, 'msg': '您不是商家身份,无法提现商家余额'}
-
- if shangjia.zhuangtai != 1:
- return {'code': 61, 'msg': '商家账号已被禁用,无法提现'}
- if shangjia.yue < jine:
- return {'code': 62, 'msg': f'商家余额不足,当前余额: {shangjia.yue}'}
-
- # 订单检查(使用 Q 取反)
- unfinished_orders = DingdanShangjia.objects.filter(
- shangjia_id=user_main.yonghuid,
- ).exclude(
- dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED,ORDER_STATUS_FAIL]
- ).exists()
- if unfinished_orders:
- return {'code': 63, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提现余额'}
-
- # 自身被处罚的罚单检查(状态不是已缴纳(2)和已驳回(4))
- self_fadan = Fadan.objects.filter(
- beichufa_id=user_main.yonghuid,
- ).exclude(
- zhuangtai__in=[2, 4]
- ).exists()
- if self_fadan:
- return {'code': 64, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'}
-
- # 申请的对他人处罚罚单检查(状态不是已缴纳(2)和已驳回(4))
- applied_fadan = Fadan.objects.filter(
- shenqing_chufa=user_main.yonghuid,
- ).exclude(
- zhuangtai__in=[2, 4]
- ).exists()
- if applied_fadan:
- return {'code': 65, 'msg': '您发起的对其他用户的处罚尚未处理完毕(待缴纳/申诉中),请等待完结'}
-
- return {'code': 0, 'msg': 'ok', 'data': {'nicheng': shangjia.nicheng or ''}}
-
- def _check_dashou_yajin_withdraw(self, user_main, jine):
- """
- 打手押金提现校验(退出平台)
- 要求:无进行中的订单,无未结算订单,自身罚单需已处理
- """
- try:
- dashou = UserDashou.objects.get(user=user_main)
- except ObjectDoesNotExist:
- return {'code': 50, 'msg': '您不是打手身份,无法提取押金'}
-
- if dashou.zhanghaozhuangtai != 1:
- return {'code': 51, 'msg': '打手账号已禁用,无法提取押金'}
- if dashou.zhuangtai != 1:
- return {'code': 52, 'msg': '您有订单进行中,请完成后提取押金'}
- if dashou.yajin < jine:
- return {'code': 53, 'msg': f'押金余额不足,当前押金: {dashou.yajin}'}
-
- # 订单检查(使用 exclude + __in)
- unfinished_orders = Dingdan.objects.filter(
- jiedan_dashou_id=user_main.yonghuid,
- ).exclude(
- zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]
- ).exists()
- if unfinished_orders:
- return {'code': 54, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提取押金'}
-
- # 自身罚单检查
- self_fadan = Fadan.objects.filter(
- beichufa_id=user_main.yonghuid,
- ).exclude(
- zhuangtai__in=[2, 4]
- ).exists()
- if self_fadan:
- return {'code': 55, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'}
-
- return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
-
-
-
-
-
-
-
-
-
-class TixianJiluHuoquViewV2(APIView):
- """
- 提现记录获取接口
- mode=1:手动提现记录(默认,兼容旧版)
- mode=2:自动提现审核记录(含 shenhe_jilu_id / shenhe_danhao,支持 zhuangtai 筛选)
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- from .models import TixianAutoRecord, TixianShenheJilu
-
- user_main = request.user
- yonghuid = user_main.yonghuid
- page = request.data.get('page', 1)
- limit = request.data.get('limit', 50)
- mode = int(request.data.get('mode', 1) or 1)
- zhuangtai_filter = int(request.data.get('zhuangtai', 0) or 0)
-
- try:
- page = int(page)
- limit = int(limit)
- if page < 1 or limit < 1:
- return Response({'code': 1, 'msg': '参数错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
- except (TypeError, ValueError):
- return Response({'code': 1, 'msg': '参数格式错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- if limit > 100:
- limit = 100
- offset = (page - 1) * limit
-
- qs = Tixianjilu.objects.filter(yonghuid=yonghuid)
- if mode == 2:
- # 自动提现:含新审核关联记录;zhuangtai>0 时按状态筛选
- if zhuangtai_filter > 0:
- qs = qs.filter(zhuangtai=zhuangtai_filter)
- qs = qs.order_by('-create_time')
-
- records_queryset = qs.values(
- 'id', 'leixing', 'jine', 'zhuangtai', 'create_time', 'fangshi',
- 'phone', 'skzhanghao', 'zhifu', 'bhliyou',
- 'shenhe_jilu_id', 'shenhe_danhao',
- )[offset:offset + limit + 1]
-
- records_list = list(records_queryset)
- has_more = len(records_list) > limit
- records_to_return = records_list[:limit] if has_more else records_list
-
- # 批量取审核表 fail_reason
- audit_ids = [r['shenhe_jilu_id'] for r in records_to_return if r.get('shenhe_jilu_id')]
- audit_map = {}
- if audit_ids:
- for a in TixianShenheJilu.objects.filter(id__in=audit_ids).values(
- 'id', 'fail_reason', 'shenqing_jine', 'shenhe_danhao'
- ):
- audit_map[a['id']] = a
-
- # 批量取自动打款失败原因
- danhao_list = [r['shenhe_danhao'] for r in records_to_return if r.get('shenhe_danhao')]
- auto_fail_map = {}
- if danhao_list:
- for ar in TixianAutoRecord.objects.filter(
- shenhe_danhao__in=danhao_list, zhuangtai=2
- ).order_by('-create_time').values('shenhe_danhao', 'fail_reason'):
- if ar['shenhe_danhao'] not in auto_fail_map:
- auto_fail_map[ar['shenhe_danhao']] = ar['fail_reason']
-
- formatted_records = []
- for record in records_to_return:
- audit = audit_map.get(record.get('shenhe_jilu_id')) if record.get('shenhe_jilu_id') else None
- fail_reason = ''
- if audit and audit.get('fail_reason'):
- fail_reason = audit['fail_reason']
- elif record.get('shenhe_danhao'):
- fail_reason = auto_fail_map.get(record['shenhe_danhao'], '') or ''
-
- jine_val = float(record['jine']) if record['jine'] else 0.00
- if mode == 2 and audit and audit.get('shenqing_jine') is not None:
- jine_val = float(audit['shenqing_jine'])
-
- shenhe_danhao_val = record.get('shenhe_danhao') or (audit.get('shenhe_danhao') if audit else '')
- formatted_records.append({
- 'id': record['id'],
- 'tixianjilu_id': record['id'],
- 'tixian_id': record['id'],
- 'leixing': record['leixing'],
- 'jine': jine_val,
- 'zhuangtai': record['zhuangtai'],
- 'create': record['create_time'].strftime('%Y-%m-%d %H:%M:%S'),
- 'fangshi': record['fangshi'],
- 'txdianhua': record['phone'] or '',
- 'txzh': record['skzhanghao'] or '',
- 'txtupian': record['zhifu'] or '',
- 'shibaiyuanyin': record['bhliyou'] or '',
- 'bhliyou': record['bhliyou'] or '',
- 'fail_reason': fail_reason,
- 'shenhe_jilu_id': record.get('shenhe_jilu_id'),
- 'shenhe_danhao': shenhe_danhao_val,
- })
-
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': {
- 'list': formatted_records,
- 'has_more': has_more,
- 'page': page,
- 'limit': limit,
- 'current_count': len(formatted_records),
- },
- })
-
- except Exception as e:
- return Response({
- 'code': 99,
- 'msg': f'系统错误: {str(e)}',
- 'data': None,
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-# yonghu/views.py
-import jwt
-import datetime
-
-
-
-class AdminLoginView(APIView):
- """
- 管理员登录接口
- 路径:/yonghu/adlog/
- 方法:POST
- 字段:zhanghao(账号), mima(密码), ip(二次验证码)
- """
-
- # 限制匿名用户访问频率,防止暴力破解
- throttle_classes = [AnonRateThrottle]
- # 允许任何人访问(登录接口不需要认证)
- permission_classes = [AllowAny]
-
- def post(self, request):
- """
- 处理管理员登录请求
- 安全策略:任何错误都返回401,不透露具体信息
- """
- try:
- # 1. 获取客户端真实IP和用户代理信息
- kehuduan_ip = huoquKehuduanIP(request)
- yonghu_daili = huoquYonghuDaili(request)
-
-
- # 2. 获取前端传递的数据
- data = request.data
-
- # 检查必需字段是否存在
- bixu_ziduan = ['zhanghao', 'mima', 'ip']
- for ziduan in bixu_ziduan:
- if ziduan not in data:
- # 字段缺失,返回401
- return Response(
- {'code': 401, 'msg': '登录失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 提取并清理数据
- zhanghao = data.get('zhanghao', '').strip()
- mima = data.get('mima', '').strip()
- erci_yanzhengma = data.get('ip', '').strip() # 二次验证码(前端叫ip)
-
- # 简单验证字段非空
- if not zhanghao or not mima or not erci_yanzhengma:
- return Response(
- {'code': 401, 'msg': '登录失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 3. 使用事务确保数据一致性
- with transaction.atomic():
- # 【优化点1】使用select_related一次性获取管理员扩展表
- # 只查询管理员类型的用户
- user = UserMain.objects.filter(
- phone=zhanghao, # 手机号作为账号
- password=mima, # 密码
- user_type='admin' # 必须是管理员类型
- ).select_related('admin_profile').first()
-
- # 如果用户不存在或不是管理员,返回401
- if not user:
- return Response(
- {'code': 401, 'msg': '登录失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 【优化点2】检查管理员扩展表是否存在
- if not hasattr(user, 'admin_profile'):
- return Response(
- {'code': 401, 'msg': '登录失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 【优化点3】验证二次密码(管理员扩展表中的password字段)
- admin_profile = user.admin_profile
- if admin_profile.password != erci_yanzhengma:
- return Response(
- {'code': 401, 'msg': '登录失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 更新用户登录信息
- # 获取当前时间
- now_time = timezone.now()
-
- # 处理IP地址长度问题
- # 如果原ip字段长度不够,截取前11个字符(保持兼容性)
-
- cunchu_ip = kehuduan_ip
- #print(cunchu_ip)
-
- # 更新用户信息
- user.ip = cunchu_ip # 存储客户端IP
- user.last_login_time = now_time # 更新最后登录时间
- user.save()
-
- # 【优化点4】生成JWT token(使用和微信登录相同的方式)
- # 使用djangorestframework-simplejwt生成token
- refresh = RefreshToken.for_user(user)
- token = str(refresh.access_token)
-
- # 5. 准备返回数据
- # 返回用户基本信息(可根据需要调整)
- response_data = {
- 'token': token,
- 'yonghuid': user.yonghuid,
- 'phone': user.phone or '',
- 'user_type': user.user_type,
- 'last_login_time': now_time.strftime('%Y-%m-%d %H:%M:%S') if now_time else '',
- }
-
- # 可以添加管理员特定信息
- if hasattr(user, 'admin_profile'):
- response_data['wechat'] = user.admin_profile.wechat or ''
-
- return Response({
- 'code': 0,
- 'msg': '登录成功',
- 'data': response_data
- })
-
- except Exception as e:
- # 【安全点】任何异常都返回401,不暴露具体错误信息
- # 记录日志用于排查问题,但前端只收到通用错误信息
- # 这里可以添加日志记录,比如:
- # import logging
- # logging.error(f'管理员登录异常: {str(e)}', exc_info=True)
-
- return Response(
- {'code': 401, 'msg': '登录失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
-
-# yonghu/views.py
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from django.db import transaction
-from django.core.exceptions import ObjectDoesNotExist
-import json
-
-# 导入模型
-from yonghu.models import UserMain, AdminProfile
-from peizhi.models import Szjilu
-
-
-class AdminFinancialDataView(APIView):
- """
- 管理员财务数据接口
- 路径:/yonghu/adhq/
- 方法:POST
- 认证:JWT Token
- 权限:仅管理员
- 返回:收支记录数据
- """
-
- # 使用JWT认证
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- """
- 处理管理员财务数据请求
- 安全策略:任何错误都返回401,不透露具体信息
- """
- try:
- # 1. 获取前端传递的数据
- data = request.data
-
- # 检查必需字段
- if 'zhanghao' not in data:
- return Response(
- {'code': 401, 'msg': '请求参数错误', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- qianzhanghao = data.get('zhanghao', '').strip()
-
- # 简单验证字段非空
- if not qianzhanghao:
- return Response(
- {'code': 401, 'msg': '请求参数错误', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 2. 获取当前登录用户(JWT认证后,request.user就是UserMain实例)
- dangqianyonghu = request.user
-
- # 3. 验证用户类型是否为admin
- if not hasattr(dangqianyonghu, 'user_type') or dangqianyonghu.user_type != 'admin':
- return Response(
- {'code': 401, 'msg': '权限不足', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证前端传递的账号与当前用户的phone字段是否一致
- if not dangqianyonghu.phone or dangqianyonghu.phone != qianzhanghao:
- return Response(
- {'code': 401, 'msg': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在(反向查询)
- # 使用高效的ORM查询,避免额外的数据库查询
- try:
- # 使用select_related一次性获取管理员扩展表
- # 注意:这里假设user_type='admin'的用户都有admin_profile
- yonghu_xiangxi = UserMain.objects.select_related('admin_profile').get(
- pk=dangqianyonghu.pk,
- user_type='admin'
- )
-
- # 检查管理员扩展表是否存在
- if not hasattr(yonghu_xiangxi, 'admin_profile'):
- return Response(
- {'code': 401, 'msg': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- except UserMain.DoesNotExist:
- return Response(
- {'code': 401, 'msg': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 查询收支记录表(Szjilu)
- # 使用高效的ORM查询,获取最新的一条记录
- # 注意:根据你的业务需求,这里获取最新的一条记录
- # 如果需要汇总多条记录,可以使用aggregate进行聚合查询
-
- # 方法1:获取最新的一条记录(假设表中只有一条汇总记录)
- shouzhijilu = Szjilu.objects.order_by('-create_time').first()
-
- # 方法2:如果表中可能有多条记录,需要汇总,可以使用以下方式:
- # from django.db.models import Sum
- # zonghe = Szjilu.objects.aggregate(
- # zongliushui=Sum('zongls'),
- # zongshouyi=Sum('zongsy'),
- # zongzhichu=Sum('zongzc'),
- # jrls=Sum('jrls'),
- # jrzc=Sum('jrzc')
- # )
-
- # 准备返回数据
- if shouzhijilu:
- # 有记录的情况
- huizongshuju = {
- 'zongliushui': float(shouzhijilu.zongls or 0),
- 'zongshouyi': float(shouzhijilu.zongsy or 0),
- 'zongzhichu': float(shouzhijilu.zongzc or 0),
- 'jrls': float(shouzhijilu.jrls or 0),
- 'jrzc': float(shouzhijilu.jrzc or 0)
- }
- else:
- # 没有记录的情况,返回默认值
- huizongshuju = {
- 'zongliushui': 0.00,
- 'zongshouyi': 0.00,
- 'zongzhichu': 0.00,
- 'jrls': 0.00,
- 'jrzc': 0.00
- }
-
- # 7. 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': huizongshuju
- })
-
- except Exception as e:
- # 【安全策略】任何异常都返回401,不暴露具体错误信息
- # 这里可以记录日志,但不返回给前端
- # import logging
- # logging.error(f'管理员财务数据接口异常: {str(e)}', exc_info=True)
-
- return Response(
- {'code': 401, 'msg': '获取失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
-
-
-
-
-
-# shangpin/views.py
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-import logging
-
-logger = logging.getLogger(__name__)
-
-
-class AdGetOrderTypes(APIView):
- """
- 后台获取订单类型列表接口
- 前端调用地址: /shangpin/getOrderTypes
- 前端需要字段: id, biaoti, tupian_url
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
-
- if not qianzhanghao:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. 身份验证(按你说的方式)
- dangqianyonghu = request.user
-
- # 验证手机号
- if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证管理员身份
- if dangqianyonghu.user_type != 'admin':
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 3. 验证管理员扩展表
- try:
- guanliyuan = dangqianyonghu.admin_profile
- except AttributeError:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 4. 查询订单类型(商品类型)表
- # 注意:这里需要根据你实际的模型名调整
- # 如果模型名不是ShangpinType,请告诉我正确的模型名
-
- # 假设模型名是ShangpinLeixing(因为我之前看到你有商品类型表)
- # 你需要告诉我确切的模型名
- try:
- # 请替换为你的实际模型名
- from shangpin.models import ShangpinLeixing # 或者 ShangpinType
- # 只查询前端需要的三个字段
- shangpin_types = ShangpinLeixing.objects.all().only('id', 'jieshao', 'tupian_url')
-
- # 构建响应数据
- type_list = [
- {
- 'id': item.id,
- 'biaoti': item.jieshao or '',
- 'tupian_url': item.tupian_url or ''
- }
- for item in shangpin_types
- ]
-
-
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': type_list
- })
-
- except Exception as e:
- logger.error(f"查询商品类型表失败: {str(e)}")
- # 如果表不存在,返回空数组
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': []
- })
-
- except Exception as e:
- logger.error(f"获取订单类型异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-
-
-# yonghu/views.py 或 dingdan/views.py
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-import logging
-from django.db.models import Q, Count
-import time
-
-logger = logging.getLogger(__name__)
-
-
-class AdGetDingdanList(APIView):
- """
- 后台订单列表获取接口
- 前端调用地址: /yonghu/adddhq
- 前端需要字段: dingdan_id, jieshao, zhuangtai, leixing, fadanpingtai, jiage, tupian_url, fadanshijian
- 统计字段: stats.platform.total, stats.platform.completed, stats.merchant.total, stats.merchant.completed
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取参数(严格按前端传的字段名)
- qianzhanghao = request.data.get('zhanghao', '').strip()
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 30))
-
- # 搜索相关参数
- dingdan_id = request.data.get('dingdan_id', '').strip()
-
- # 筛选相关参数(严格按前端字段名)
- fadanpingtai = request.data.get('fadanpingtai') # 前端叫 fadanpingtai
- leixing = request.data.get('leixing') # 前端叫 leixing
- zhuangtai = request.data.get('zhuangtai', '') # 前端叫 zhuangtai
-
- if not qianzhanghao:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. 身份验证(与你说的完全一样)
- dangqianyonghu = request.user
-
- # 验证手机号
- if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证管理员身份
- if dangqianyonghu.user_type != 'admin':
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 3. 验证管理员扩展表
- try:
- guanliyuan = dangqianyonghu.admin_profile
- except AttributeError:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 4. 导入订单模型
- from dingdan.models import Dingdan # 你的订单主表模型
-
- # 5. 构建查询条件(严格按前端字段名映射到数据库字段)
- query_conditions = Q()
-
- # 判断是否为搜索模式
- is_search_mode = bool(dingdan_id)
-
- if is_search_mode:
- # 搜索模式:只按订单ID查询
- query_conditions &= Q(dingdan_id=dingdan_id)
- else:
- # 筛选模式
-
- # 发单平台筛选(前端: fadanpingtai -> 数据库: fadan_pingtai)
- if fadanpingtai is not None:
- try:
- fadanpingtai_int = int(fadanpingtai)
- if fadanpingtai_int in [1, 2]:
- query_conditions &= Q(fadan_pingtai=fadanpingtai_int)
- except (ValueError, TypeError):
- pass
-
- # 订单类型筛选(前端: leixing -> 数据库: leixing_id)
- if leixing is not None:
- try:
- leixing_int = int(leixing)
- query_conditions &= Q(leixing_id=leixing_int)
- except (ValueError, TypeError):
- pass
-
- # 订单状态筛选(前端: zhuangtai -> 数据库: zhuangtai)
- if zhuangtai and zhuangtai != 'all':
- try:
- # 处理多个状态,如 "1,7"
- status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
- if status_list:
- query_conditions &= Q(zhuangtai__in=status_list)
- except (ValueError, TypeError):
- pass
-
- # 6. 获取统计数据(平台订单和商家订单)
- # 注意:这里统计所有订单,不考虑筛选条件
- try:
- # 平台订单统计 (fadan_pingtai=1)
- platform_total = Dingdan.objects.filter(fadan_pingtai=1).count()
- platform_completed = Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=3).count()
-
- # 商家订单统计 (fadan_pingtai=2)
- merchant_total = Dingdan.objects.filter(fadan_pingtai=2).count()
- merchant_completed = Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=3).count()
-
- platform_stats = {
- 'total': platform_total,
- 'completed': platform_completed
- }
- merchant_stats = {
- 'total': merchant_total,
- 'completed': merchant_completed
- }
- except Exception as e:
- logger.warning(f"统计数据查询失败: {str(e)}")
- platform_stats = {'total': 0, 'completed': 0}
- merchant_stats = {'total': 0, 'completed': 0}
-
- # 7. 执行分页查询(只查询主表,不查扩展表)
- start_time = time.time()
-
- # 计算分页
- offset = (page - 1) * page_size
-
- # 查询符合条件的订单总数
- total_count = Dingdan.objects.filter(query_conditions).count()
-
- # 查询当前页的数据(只查询主表,只查询前端需要的字段)
- dingdan_list = Dingdan.objects.filter(query_conditions).only(
- 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id',
- 'fadan_pingtai', 'jine', 'tupian', 'create_time'
- ).order_by('-create_time')[offset:offset + page_size]
-
- query_time = time.time() - start_time
- logger.info(f"订单查询完成,耗时: {query_time:.3f}s,查询条数: {len(dingdan_list)}")
-
- # 8. 构建响应数据(严格按前端需要的字段)
- result_list = []
-
- for dingdan in dingdan_list:
- # 注意:这里字段名必须与前端完全一致
- order_data = {
- 'dingdan_id': dingdan.dingdan_id or '',
- 'jieshao': dingdan.jieshao or '',
- 'zhuangtai': dingdan.zhuangtai or 1,
- 'leixing': dingdan.leixing_id or 0, # 前端需要 leixing
- 'fadanpingtai': dingdan.fadan_pingtai or 1, # 前端需要 fadanpingtai
- 'jiage': float(dingdan.jine) if dingdan.jine else 0.00, # 价格对应 jine 字段
- 'tupian_url': dingdan.tupian or '', # 图片对应 tupian 字段
- 'fadanshijian': dingdan.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan.create_time else ''
- }
- result_list.append(order_data)
-
- # 9. 判断是否有更多数据
- current_loaded_count = offset + len(result_list)
- has_more = len(result_list) >= page_size and current_loaded_count < total_count
-
- # 10. 构建完整响应(严格按前端需要的结构)
- response_data = {
- 'stats': {
- 'platform': platform_stats,
- 'merchant': merchant_stats
- },
- 'list': result_list,
- 'hasMore': has_more,
- 'total': total_count,
- 'currentPage': page,
- 'pageSize': page_size
- }
-
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': response_data
- })
-
- except Exception as e:
- logger.error(f"获取订单列表异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-
-
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-from rest_framework import status
-from django.db import transaction
-import logging
-
-# 获取日志器
-logger = logging.getLogger(__name__)
-
-
-class AdYaoQingDaShou(APIView):
- """
- 后台获取邀请码接口(用于邀请打手)
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /yonghu/adyqds
- 参数: {
- "zhanghao": "管理员账号"
- }
- 返回: {
- "code": 0,
- "message": "成功",
- "data": {
- "yaoqingma": "邀请码"
- }
- }
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
-
- if not qianzhanghao:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.user_type != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.admin_profile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 获取用户ID(yonghuid),如果获取不到就用'123456'
- yonghuid_str = getattr(dangqianyonghu, 'yonghuid', '123456')
-
- # 7. 尝试获取或创建管事扩展表
- try:
- # 尝试获取现有的管事扩展表
- guanshi_profile = dangqianyonghu.guanshi_profile
- except AttributeError:
- # 管事扩展表不存在,创建一个
- try:
- from yonghu.models import UserGuanshi
-
- # 创建默认的管事扩展表
- guanshi_profile = UserGuanshi.objects.create(
- user=dangqianyonghu,
- yaoqingma='', # 暂时为空,后面生成
- dianhua='', # 默认空
- wechat='', # 默认空
- yaogingshuliang=0, # 邀请数量为0
- zhuangtai=1, # 正常状态
- jinrichongzhi=0, # 今日充值0
- jinyuechongzhi=0, # 今月充值0
- chongzhifenrun=0, # 充值分佣0
- yue=0 # 余额0
- )
- #logger.info(f"为管理员{dangqianyonghu.yonghuid}创建了管事扩展表")
- except Exception as e:
- logger.error(f"创建管事扩展表失败: {str(e)}")
- return Response(
- {'code': 500, 'message': '创建用户信息失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- # 8. 检查是否已有邀请码
- existing_yaoqingma = guanshi_profile.yaoqingma
-
- if existing_yaoqingma and existing_yaoqingma.strip():
- # 已有邀请码,直接返回
- return Response({
- 'code': 0,
- 'message': '成功获取邀请码',
- 'data': {
- 'yaoqingma': existing_yaoqingma
- }
- })
-
- # 9. 生成新的邀请码
- try:
- # 导入邀请码生成工具
- from utils.yaoqingma_generator import chuangjianYaoqingma
-
- # 生成邀请码
- new_yaoqingma = chuangjianYaoqingma(str(yonghuid_str))
-
- # 验证邀请码格式
- from utils.yaoqingma_generator import yanzhengYaoqingmaWeiyixing
- if not yanzhengYaoqingmaWeiyixing(new_yaoqingma):
- raise Exception('邀请码格式验证失败')
-
- except ImportError:
- # 如果导入失败,使用简单方法生成邀请码
- import time
- import random
- import string
-
- timestamp = str(int(time.time()))
- random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
- new_yaoqingma = f"GL{timestamp[-6:]}{random_str}"
-
- logger.warning(f"使用简单方法生成邀请码: {new_yaoqingma}")
- except Exception as e:
- logger.error(f"生成邀请码失败: {str(e)}")
- return Response(
- {'code': 500, 'message': '生成邀请码失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- # 10. 保存邀请码到数据库
- try:
- with transaction.atomic():
- guanshi_profile.yaoqingma = new_yaoqingma
- guanshi_profile.save()
-
- logger.info(f"管理员{dangqianyonghu.yonghuid}生成邀请码: {new_yaoqingma}")
- except Exception as e:
- logger.error(f"保存邀请码失败: {str(e)}")
- return Response(
- {'code': 500, 'message': '保存邀请码失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- # 11. 返回成功响应
- return Response({
- 'code': 0,
- 'message': '成功生成邀请码',
- 'data': {
- 'yaoqingma': new_yaoqingma
- }
- })
-
- except Exception as e:
- logger.error(f"获取邀请码接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-from rest_framework import status
-from django.db.models import Q
-import logging
-
-logger = logging.getLogger(__name__)
-
-
-class AdGuanLiYongHu(APIView):
- """
- 后台用户管理列表接口
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /yonghu/adgl
- 参数: {
- "zhanghao": "管理员账号",
- "user_type": "用户类型(1-4)",
- "keyword": "搜索关键词(可选)",
- "page": "页码(从1开始)",
- "page_size": "每页数量"
- }
- 返回: {
- "code": 0,
- "message": "成功",
- "data": {
- "list": [用户列表],
- "stats": {"1": 总数, "2": 总数, "3": 总数, "4": 总数},
- "has_more": true/false
- }
- }
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
- user_type = request.data.get('user_type')
- keyword = request.data.get('keyword', '').strip()
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 30))
-
- # 验证参数
- if not qianzhanghao or not user_type:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.user_type != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.admin_profile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 根据用户类型选择查询的模型
- user_type_int = int(user_type)
-
- if user_type_int not in [1, 2, 3, 4]:
- return Response(
- {'code': 400, 'message': '用户类型错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 7. 统计四种用户类型的总数
- stats = self._get_user_stats()
-
- # 8. 根据用户类型查询用户列表
- user_list, has_more = self._query_users_by_type(
- user_type_int, keyword, page, page_size
- )
-
- # 9. 构建返回数据
- return Response({
- 'code': 0,
- 'message': '成功',
- 'data': {
- 'list': user_list,
- 'stats': stats,
- 'has_more': has_more
- }
- })
-
- except ValueError:
- logger.error("参数类型错误")
- return Response(
- {'code': 400, 'message': '参数类型错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- logger.error(f"用户管理列表接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- def _get_user_stats(self):
- """
- 获取四种用户类型的统计数量
- """
- try:
- from yonghu.models import UserBoss, UserDashou, UserGuanshi, UserShangjia
-
- stats = {
- '1': UserBoss.objects.count(),
- '2': UserDashou.objects.count(),
- '3': UserGuanshi.objects.count(),
- '4': UserShangjia.objects.count()
- }
-
- return stats
-
- except Exception as e:
- logger.error(f"获取用户统计失败: {str(e)}")
- return {'1': 0, '2': 0, '3': 0, '4': 0}
-
- def _query_users_by_type(self, user_type_int, keyword, page, page_size):
- """
- 根据用户类型查询用户列表
- 返回: (用户列表, 是否还有更多数据)
- """
- try:
- # 计算偏移量
- offset = (page - 1) * page_size
-
- if user_type_int == 1:
- return self._query_boss_users(keyword, offset, page_size)
- elif user_type_int == 2:
- return self._query_dashou_users(keyword, offset, page_size)
- elif user_type_int == 3:
- return self._query_guanshi_users(keyword, offset, page_size)
- elif user_type_int == 4:
- return self._query_shangjia_users(keyword, offset, page_size)
- else:
- return [], False
-
- except Exception as e:
- logger.error(f"查询用户列表失败: {str(e)}")
- return [], False
-
- def _query_boss_users(self, keyword, offset, limit):
- """查询普通用户(老板)"""
- try:
- from yonghu.models import UserBoss
-
- queryset = UserBoss.objects.select_related('user')
-
- if keyword:
- queryset = queryset.filter(
- Q(user__yonghuid__icontains=keyword) |
- Q(nickname__icontains=keyword)
- )
-
- # 获取总数
- total_count = queryset.count()
-
- # 获取当前页的数据
- users = list(queryset.order_by('-create_time')[offset:offset + limit])
-
- # 计算是否有更多数据
- current_count = len(users)
- has_more = (offset + current_count) < total_count
-
-
-
- # 构建返回数据
- user_list = []
- for boss in users:
- user_main = boss.user
-
- user_data = {
- 'yonghuid': user_main.yonghuid,
- 'avatar': user_main.avatar or '',
- 'nicheng': boss.nickname or '',
- }
-
- user_list.append(user_data)
-
- return user_list, has_more
-
- except Exception as e:
- logger.error(f"查询老板用户失败: {str(e)}")
- return [], False
-
- def _query_dashou_users(self, keyword, offset, limit):
- """查询打手用户"""
- try:
- from yonghu.models import UserDashou
-
- queryset = UserDashou.objects.select_related('user')
-
- if keyword:
- queryset = queryset.filter(
- Q(user__yonghuid__icontains=keyword) |
- Q(nicheng__icontains=keyword) |
- Q(chenghao__icontains=keyword)
- )
-
- # 获取总数
- total_count = queryset.count()
-
- # 获取当前页的数据
- dashou_list = list(queryset.order_by('-create_time')[offset:offset + limit])
-
- # 计算是否有更多数据
- current_count = len(dashou_list)
- has_more = (offset + current_count) < total_count
-
-
-
- # 构建返回数据
- user_list = []
- for dashou in dashou_list:
- user_main = dashou.user
-
- user_data = {
- 'yonghuid': user_main.yonghuid,
- 'avatar': user_main.avatar or '',
- 'zaixianzhuangtai': dashou.zaixianzhuangtai,
- 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
- 'nicheng': dashou.nicheng or '',
- 'zhuangtai': dashou.zhuangtai,
- }
-
- user_list.append(user_data)
-
- return user_list, has_more
-
- except Exception as e:
- logger.error(f"查询打手用户失败: {str(e)}")
- return [], False
-
- def _query_guanshi_users(self, keyword, offset, limit):
- """查询管事用户"""
- try:
- from yonghu.models import UserGuanshi
-
- queryset = UserGuanshi.objects.select_related('user')
-
- if keyword:
- queryset = queryset.filter(
- Q(user__yonghuid__icontains=keyword)
- )
-
- # 获取总数
- total_count = queryset.count()
-
- # 获取当前页的数据
- guanshi_list = list(queryset.order_by('-create_time')[offset:offset + limit])
-
- # 计算是否有更多数据
- current_count = len(guanshi_list)
- has_more = (offset + current_count) < total_count
-
-
-
- # 构建返回数据
- user_list = []
- for guanshi in guanshi_list:
- user_main = guanshi.user
-
- user_data = {
- 'yonghuid': user_main.yonghuid,
- 'avatar': user_main.avatar or '',
- 'zhanghaozhuangtai': guanshi.zhuangtai,
- 'nicheng': f'管事{user_main.yonghuid}',
- }
-
- user_list.append(user_data)
-
- return user_list, has_more
-
- except Exception as e:
- logger.error(f"查询管事用户失败: {str(e)}")
- return [], False
-
- def _query_shangjia_users(self, keyword, offset, limit):
- """查询商家用户"""
- try:
- from yonghu.models import UserShangjia
-
- queryset = UserShangjia.objects.select_related('user')
-
- if keyword:
- queryset = queryset.filter(
- Q(user__yonghuid__icontains=keyword) |
- Q(nicheng__icontains=keyword)
- )
-
- # 获取总数
- total_count = queryset.count()
-
- # 获取当前页的数据
- shangjia_list = list(queryset.order_by('-create_time')[offset:offset + limit])
-
- # 计算是否有更多数据
- current_count = len(shangjia_list)
- has_more = (offset + current_count) < total_count
-
-
-
- # 构建返回数据
- user_list = []
- for shangjia in shangjia_list:
- user_main = shangjia.user
-
- user_data = {
- 'yonghuid': user_main.yonghuid,
- 'avatar': user_main.avatar or '',
- 'zhanghaozhuangtai': shangjia.zhuangtai,
- 'nicheng': shangjia.nicheng or '',
- }
-
- user_list.append(user_data)
-
- return user_list, has_more
-
- except Exception as e:
- logger.error(f"查询商家用户失败: {str(e)}")
- return [], False
-
-
-
-
-
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-from rest_framework import status
-from django.db.models import Q
-import logging
-
-logger = logging.getLogger(__name__)
-
-
-class CfGuanLi(APIView):
- """
- 后台处罚管理列表接口
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /yonghu/cfgl
- 参数: {
- "zhanghao": "管理员账号",
- "qingqiu_tongji": true/false, # 是否只请求统计信息
- "sqzhuangtai": 0 或 [1,2], # 处罚申请状态筛选(可选)
- "page": "页码(从1开始)", # 分页参数
- "page_size": "每页数量" # 分页参数
- }
- 返回: {
- "code": 0,
- "message": "成功",
- "data": {
- "list": [处罚记录列表],
- "zongshu": 总数,
- "daichuli": 待处理数量,
- "yichuli": 已处理数量,
- "haiyougengduo": true/false
- }
- }
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
- qingqiu_tongji = request.data.get('qingqiu_tongji', False)
- sqzhuangtai = request.data.get('sqzhuangtai')
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 30))
-
- # 验证参数
- if not qianzhanghao:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.user_type != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.admin_profile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 导入模型
- from dingdan.models import Chufajilu
-
- # 7. 获取统计信息(总是需要统计)
- zongshu = Chufajilu.objects.count()
- daichuli = Chufajilu.objects.filter(sqzhuangtai__in=[0, 3]).count()
- yichuli = Chufajilu.objects.filter(sqzhuangtai__in=[1, 2]).count()
-
- # 8. 如果只请求统计信息,直接返回
- if qingqiu_tongji:
- return Response({
- 'code': 0,
- 'message': '成功',
- 'data': {
- 'zongshu': zongshu,
- 'daichuli': daichuli,
- 'yichuli': yichuli
- }
- })
-
- # 9. 构建查询集
- queryset = Chufajilu.objects.all()
-
- # 10. 根据状态筛选
- if sqzhuangtai is not None:
- if isinstance(sqzhuangtai, list):
- # 如果是数组,表示多个状态
- queryset = queryset.filter(sqzhuangtai__in=sqzhuangtai)
- else:
- # 单个状态
- queryset = queryset.filter(sqzhuangtai=sqzhuangtai)
-
- # 11. 获取总数(用于分页计算)
- total_count = queryset.count()
-
- # 12. 计算分页偏移量
- offset = (page - 1) * page_size
-
- # 13. 分页查询
- chufa_list = list(queryset.order_by('-create_time')[offset:offset + page_size])
-
- # 🔴【新增】高效获取图片信息
- if chufa_list:
- # 收集所有需要查询的组合
- dingdan_ids = []
- yonghu_combinations = [] # 保存 (dingdan_id, yonghuid) 组合
-
- for chufa in chufa_list:
- if chufa.dingdan_id:
- dingdan_ids.append(chufa.dingdan_id)
- # 商家图片组合
- if chufa.qingqiuid:
- yonghu_combinations.append((chufa.dingdan_id, chufa.qingqiuid))
- # 打手图片组合
- if chufa.dashouid:
- yonghu_combinations.append((chufa.dingdan_id, chufa.dashouid))
-
- # 去重
- dingdan_ids = list(set(dingdan_ids))
- yonghu_combinations = list(set(yonghu_combinations))
-
- # 🔴【新增】批量查询图片表(高效查询,避免循环)
- tupian_mapping = {}
- if yonghu_combinations:
- # 导入图片模型
- from dingdan.models import Chufatupian
-
- # 构建查询条件
- from django.db.models import Q
- query_conditions = Q()
- for dingdan_id, yonghuid in yonghu_combinations:
- query_conditions |= Q(dingdan_id=dingdan_id, yonghuid=yonghuid)
-
- # 批量查询
- tupian_queryset = Chufatupian.objects.filter(query_conditions).values(
- 'dingdan_id', 'yonghuid', 'tupian'
- )
-
- # 构建映射:key为"dingdan_id_yonghuid",value为图片URL列表
- for item in tupian_queryset:
- key = f"{item['dingdan_id']}_{item['yonghuid']}"
- if key not in tupian_mapping:
- tupian_mapping[key] = []
- if item['tupian']:
- tupian_mapping[key].append(item['tupian'])
- else:
- tupian_mapping = {}
-
- # 🔴【修改】14. 构建返回数据 - 添加图片和申诉理由字段
- chufa_data = []
- for chufa in chufa_list:
- # 🔴【新增】获取商家证据图片
- zhengju_tupian = []
- if chufa.dingdan_id and chufa.qingqiuid:
- key = f"{chufa.dingdan_id}_{chufa.qingqiuid}"
- zhengju_tupian = tupian_mapping.get(key, [])
-
- # 🔴【新增】获取打手申诉图片
- shensu_tupian = []
- if chufa.dingdan_id and chufa.dashouid:
- key = f"{chufa.dingdan_id}_{chufa.dashouid}"
- shensu_tupian = tupian_mapping.get(key, [])
-
- chufa_data.append({
- 'dashouid': chufa.dashouid,
- 'qingqiuid': chufa.qingqiuid,
- 'chuliid': chufa.chuliid,
- 'cfliyou': chufa.cfliyou,
- 'sqzhuangtai': chufa.sqzhuangtai,
- 'bhliyou': chufa.bhliyou,
- 'jifen': chufa.jifen,
- 'dingdan_id': chufa.dingdan_id,
- 'create_time': chufa.create_time,
- 'update_time': chufa.update_time,
- # 🔴【新增】申诉理由字段
- 'ssliyou': chufa.ssliyou or '',
- # 🔴【新增】商家证据图片
- 'zhengju_tupian': zhengju_tupian,
- # 🔴【新增】打手申诉图片
- 'shensu_tupian': shensu_tupian,
- })
-
- # 15. 计算是否还有更多数据
- current_count = len(chufa_list)
- haiyougengduo = (offset + current_count) < total_count
-
- return Response({
- 'code': 0,
- 'message': '成功',
- 'data': {
- 'list': chufa_data,
- 'zongshu': zongshu,
- 'daichuli': daichuli,
- 'yichuli': yichuli,
- 'haiyougengduo': haiyougengduo
- }
- })
-
- except ValueError:
- return Response(
- {'code': 400, 'message': '参数类型错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- logger.error(f"处罚管理列表接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-logger = logging.getLogger(__name__)
-
-
-class AdTongYiChuFa(APIView):
- """
- 后台同意/驳回处罚接口(已修复重复记录问题)
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /yonghu/adtycf
- 参数: {
- "zhanghao": "管理员账号",
- "dingdan_id": "订单ID",
- "caozuo": 1或2, # 1=同意处罚,2=拒绝处罚
- "bohui_liyou": "拒绝理由" # 拒绝处罚时必填
- }
- 返回: {
- "code": 0,
- "message": "成功",
- "data": null
- }
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- caozuo = request.data.get('caozuo') # 1=同意,2=拒绝
- bohui_liyou = request.data.get('bohui_liyou', '').strip()
-
- # 验证参数
- if not qianzhanghao or not dingdan_id or caozuo is None:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证操作类型
- if caozuo not in [1, 2]:
- return Response(
- {'code': 400, 'message': '操作类型错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 如果是拒绝处罚,需要拒绝理由
- if caozuo == 2 and not bohui_liyou:
- return Response(
- {'code': 400, 'message': '拒绝处罚需要填写理由', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.user_type != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.admin_profile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 使用事务保证数据一致性
- with transaction.atomic():
- # 7. 导入模型
- from dingdan.models import Chufajilu, Dingdan, DingdanShangjia
- from yonghu.models import UserDashou
-
- # 8. 🔴 修复:查询处罚记录(处理重复记录问题)
- # 使用 filter().first() 而不是 get(),以防出现多条记录
- chufajilu = Chufajilu.objects.filter(
- dingdan_id=dingdan_id,
- sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中状态
- ).order_by('-create_time').first() # 取最新的一条
-
- if not chufajilu:
- # 如果没有待处理的处罚记录,检查是否有已处理的
- exists_processed = Chufajilu.objects.filter(dingdan_id=dingdan_id).exists()
- if exists_processed:
- return Response(
- {'code': 400, 'message': '该处罚记录已处理', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- else:
- return Response(
- {'code': 404, 'message': '处罚记录不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 9. 验证处罚记录状态(必须是待处理或申诉中状态才能处理)
- if chufajilu.sqzhuangtai not in [0, 3]: # 🔴【修改】包括待处理(0)和申诉中(3)
- return Response(
- {'code': 400, 'message': '该处罚记录已处理', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 10. 🔴 修复:检查是否有重复的待处理/申诉中记录,并记录警告
- duplicate_count = Chufajilu.objects.filter(
- dingdan_id=dingdan_id,
- sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中
- ).count()
-
- if duplicate_count > 1:
- # 记录警告日志
- logger.warning(f"订单ID {dingdan_id} 存在 {duplicate_count} 条待处理/申诉中处罚记录,处理最新的一条")
-
- # 🔴 可选:将其他重复的待处理/申诉中记录标记为已拒绝
- # 防止用户重复提交相同订单的处罚申请
- duplicate_records = Chufajilu.objects.filter(
- dingdan_id=dingdan_id,
- sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中
- ).exclude(id=chufajilu.id)
-
- for dup in duplicate_records:
- dup.sqzhuangtai = 2 # 已拒绝
- dup.chuliid = qianzhanghao
- dup.bhliyou = "系统自动拒绝:存在重复处罚记录"
- dup.save()
-
- # 11. 根据操作类型处理
- if caozuo == 1:
- # 同意处罚 - 不扣分,只改状态
- # 11.1 检查打手是否存在
- if not chufajilu.dashouid:
- return Response(
- {'code': 400, 'message': '被处罚打手ID不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 11.2 查询打手扩展表(只是验证存在性)
- try:
- dashou = UserDashou.objects.get(user__yonghuid=chufajilu.dashouid)
- except UserDashou.DoesNotExist:
- return Response(
- {'code': 404, 'message': '被处罚打手不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 🔴【修改】11.3 同意处罚时不扣分(因为申请时已经扣了),只更新状态
- chufajilu.sqzhuangtai = 1 # 已处罚
- chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号
- chufajilu.save()
-
- # 11.4 更新商家订单扩展表
- try:
- dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
- if hasattr(dingdan, 'shangjia_kuozhan'):
- shangjia_kuozhan = dingdan.shangjia_kuozhan
- shangjia_kuozhan.sqzhuangtai = 1 # 已处罚
- shangjia_kuozhan.save()
- except (Dingdan.DoesNotExist, AttributeError):
- # 如果订单或扩展表不存在,继续执行,不中断
- pass
-
- logger.info(f"同意处罚成功:订单ID={dingdan_id},打手ID={chufajilu.dashouid}")
-
- elif caozuo == 2:
- # 拒绝处罚 - 需要返还积分(加分)
- # 🔴【新增】11.5 返还打手积分(加5分)
- if chufajilu.jifen > 0:
- try:
- dashou = UserDashou.objects.get(user__yonghuid=chufajilu.dashouid)
-
- # 🔴【新增】安全验证:必须积分小于等于5分才允许加分
- if dashou.jifen <= 5: # 判断积分是否小于等于5分
- dashou.jifen += chufajilu.jifen # 加5分
- dashou.save()
- logger.info(
- f"拒绝处罚,给打手{dashou.user.yonghuid}返还积分{chufajilu.jifen}分,当前积分{dashou.jifen}")
- else:
- # 积分已超过5分,可能是异常情况,记录警告但不中断处理
- logger.warning(
- f"拒绝处罚时打手积分异常:打手ID={chufajilu.dashouid},当前积分{dashou.jifen}已超过5分,不再加分")
- except UserDashou.DoesNotExist:
- # 打手不存在,记录错误,但继续处理状态
- logger.error(f"拒绝处罚时打手不存在:打手ID={chufajilu.dashouid}")
-
- # 11.6 更新处罚记录
- chufajilu.sqzhuangtai = 2 # 已拒绝
- chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号
- chufajilu.bhliyou = bohui_liyou # 拒绝理由
- chufajilu.save()
-
- # 11.7 更新商家订单扩展表
- try:
- dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
- if hasattr(dingdan, 'shangjia_kuozhan'):
- shangjia_kuozhan = dingdan.shangjia_kuozhan
- shangjia_kuozhan.sqzhuangtai = 2 # 已拒绝
- shangjia_kuozhan.bhliyou = bohui_liyou # 拒绝理由
- shangjia_kuozhan.save()
- except (Dingdan.DoesNotExist, AttributeError):
- # 如果订单或扩展表不存在,继续执行,不中断
- pass
-
- logger.info(f"拒绝处罚成功:订单ID={dingdan_id},拒绝理由={bohui_liyou}")
-
- # 12. 返回成功响应
- return Response({
- 'code': 0,
- 'message': '处理成功',
- 'data': None
- })
-
- except ValueError:
- return Response(
- {'code': 400, 'message': '参数类型错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- logger.error(f"处理处罚接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-
-# yonghu/views.py
-from django.utils import timezone
-from datetime import timedelta
-from decimal import Decimal
-from django.db import transaction
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework import status
-from .models import (
- UserMain, UserBoss, UserDashou, UserShangjia, UserGuanshi,
- AdminProfile, Xiugaijilu
-)
-from shangpin.models import Huiyuan, Huiyuangoumai
-
-
-class AdckyhxqView(APIView):
- """
- 获取用户详情接口
- 前端需要传递: zhanghao, uid, user_type
- 返回用户信息和会员列表
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # ==================== 管理员身份验证 ====================
- # 1. 验证JWT Token
- if not request.user.is_authenticated:
- return Response({
- 'code': 401,
- 'message': '未认证',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 验证用户类型为管理员
- if request.user.user_type != 'admin':
- return Response({
- 'code': 403,
- 'message': '权限不足',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 3. 验证管理员扩展表存在
- admin_profile = getattr(request.user, 'admin_profile', None)
- if not admin_profile:
- return Response({
- 'code': 403,
- 'message': '管理员信息不完整',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 4. 验证管理员账号(phone)与前端传递的zhanghao一致
- zhanghao = request.data.get('zhanghao')
- if not zhanghao or request.user.phone != zhanghao:
- return Response({
- 'code': 401,
- 'message': '账号验证失败',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
- # ==================== 身份验证结束 ====================
-
- # 获取业务参数
- uid = request.data.get('uid') # 用户ID (yonghuid)
- user_type = request.data.get('user_type') # 用户类型 (1,2,3,4)
-
- if not uid or not user_type:
- return Response({
- 'code': 400,
- 'message': '参数缺失',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证用户类型
- try:
- user_type = int(user_type)
- if user_type not in [1, 2, 3, 4]:
- raise ValueError
- except ValueError:
- return Response({
- 'code': 400,
- 'message': '用户类型无效',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 查询用户主表 - 使用select_related优化查询
- user_main = UserMain.objects.filter(yonghuid=uid).first()
- if not user_main:
- return Response({
- 'code': 404,
- 'message': '用户不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 构建响应数据
- response_data = {
- 'user_info': {},
- 'member_list': [],
- 'all_member_list': []
- }
-
- # 通用字段(所有用户都有)
- user_info = {
- 'yonghuid': user_main.yonghuid,
- 'avatar': user_main.avatar,
- 'phone': user_main.phone,
- 'ip': user_main.ip,
- 'create_time': user_main.create_time,
- 'update_time': user_main.update_time,
- 'user_type': user_type
- }
-
- # 根据用户类型查询扩展信息
- if user_type == 1: # 老板
- # 使用select_related预取boss_profile
- user_main = UserMain.objects.filter(yonghuid=uid).select_related('boss_profile').first()
- if user_main.boss_profile:
- boss_profile = user_main.boss_profile
- user_info.update({
- 'nickname': boss_profile.nickname,
- 'zonge': boss_profile.zonge,
- 'alldingdan': boss_profile.alldingdan,
- 'alltui': boss_profile.alltui,
- 'create_time_boss': boss_profile.create_time
- })
-
- elif user_type == 2: # 打手
- # 使用select_related预取dashou_profile
- user_main = UserMain.objects.filter(yonghuid=uid).select_related('dashou_profile').first()
- if user_main.dashou_profile:
- dashou_profile = user_main.dashou_profile
-
- # 构建打手信息
- user_info.update({
- 'nicheng': dashou_profile.nicheng,
- 'chenghao': dashou_profile.chenghao,
- 'zhuangtai': dashou_profile.zhuangtai,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
- 'jieshao': dashou_profile.jieshao,
- 'jiedanzongliang': dashou_profile.jiedanzongliang,
- 'chengjiaozongliang': dashou_profile.chengjiaozongliang,
- 'tuikuanliang': dashou_profile.tuikuanliang,
- 'yue': dashou_profile.yue, # 打手可提现余额
- 'zonge': dashou_profile.zonge, # 打手赚取总额
- 'dianhua': dashou_profile.dianhua,
- 'wechat': dashou_profile.wechat,
- 'yaoqingren': dashou_profile.yaoqingren,
- 'jinrijiedan': dashou_profile.jinrijiedan,
- 'jinrishouyi': dashou_profile.jinrishouyi,
- 'jinyuejiedan': dashou_profile.jinyuejiedan,
- 'jinyueshouyi': dashou_profile.jinyueshouyi,
- 'jifen': dashou_profile.jifen,
- 'yajin': dashou_profile.yajin,
- 'create_time_dashou': dashou_profile.create_time
- })
-
- # 查询打手的会员信息(只返回未过期的)
- # 使用select_related预取huiyuan信息(如果有关联的话)
- member_records = Huiyuangoumai.objects.filter(
- yonghu_id=uid
- ).order_by('-create_time')
-
- member_list = []
- for record in member_records:
- # 调用方法检查是否过期,并更新状态
- is_expired = record.jiance_shifou_daoqi()
-
- if not is_expired: # 只返回未过期的
- # 查询会员详情
- huiyuan = Huiyuan.objects.filter(
- huiyuan_id=record.huiyuan_id
- ).first()
-
- if huiyuan:
- member_list.append({
- 'huiyuan_id': record.huiyuan_id,
- 'huiyuan_name': huiyuan.jieshao,
- 'daoqi_time': record.daoqi_time,
- 'is_active': record.huiyuan_zhuangtai == 1,
- 'create_time': record.create_time
- })
-
- response_data['member_list'] = member_list
-
- elif user_type == 3: # 管事
- # 使用select_related预取guanshi_profile
- user_main = UserMain.objects.filter(yonghuid=uid).select_related('guanshi_profile').first()
- if user_main.guanshi_profile:
- guanshi_profile = user_main.guanshi_profile
- user_info.update({
- 'nicheng': user_main.phone, # 管事昵称用手机号
- 'yaoqingma': guanshi_profile.yaoqingma,
- 'dianhua': guanshi_profile.dianhua,
- 'wechat': guanshi_profile.wechat,
- 'yaogingshuliang': guanshi_profile.yaogingshuliang,
- 'zhuangtai': guanshi_profile.zhuangtai,
- 'jinrichongzhi': guanshi_profile.jinrichongzhi,
- 'jinyuechongzhi': guanshi_profile.jinyuechongzhi,
- 'chongzhifenrun': guanshi_profile.chongzhifenrun,
- 'yue': guanshi_profile.yue, # 管事可提现余额
- 'create_time_guanshi': guanshi_profile.create_time
- })
-
- elif user_type == 4: # 商家
- # 使用select_related预取shop_profile
- user_main = UserMain.objects.filter(yonghuid=uid).select_related('shop_profile').first()
- if user_main.shop_profile:
- shop_profile = user_main.shop_profile
- user_info.update({
- 'nicheng': shop_profile.nicheng,
- 'zhuangtai': shop_profile.zhuangtai,
- 'dianhua': shop_profile.dianhua,
- 'wechat': shop_profile.wechat,
- 'fabu': shop_profile.fabu,
- 'tuikuan': shop_profile.tuikuan,
- 'yue': shop_profile.yue, # 商家余额
- 'chengjiao': shop_profile.chengjiao,
- 'jinridingdan': shop_profile.jinridingdan,
- 'jinriliushui': shop_profile.jinriliushui,
- 'jinyuedingdan': shop_profile.jinyuedingdan,
- 'jinyueliushui': shop_profile.jinyueliushui,
- 'create_time_shop': shop_profile.create_time
- })
-
- # 获取所有会员列表(用于前端下拉选择)
- all_huiyuan = Huiyuan.objects.all().order_by('jiage')
- all_member_list = [
- {
- 'id': huiyuan.huiyuan_id,
- 'nicheng': huiyuan.jieshao,
- 'jiage': huiyuan.jiage,
- 'jieshao': huiyuan.jtjieshao
- }
- for huiyuan in all_huiyuan
- ]
-
- response_data['user_info'] = user_info
- response_data['all_member_list'] = all_member_list
-
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': response_data
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- return Response({
- 'code': 500,
- 'message': f'获取用户详情失败: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class AdcjxgView(APIView):
- """
- 修改用户信息接口
- 前端需要传递: zhanghao, uid, user_type, 以及其他修改的字段
- 支持打手会员添加功能
- 敏感字段修改会记录到修改记录表
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # ==================== 管理员身份验证 ====================
- if not request.user.is_authenticated:
- return Response({
- 'code': 401,
- 'message': '未认证',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- if request.user.user_type != 'admin':
- return Response({
- 'code': 403,
- 'message': '权限不足',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- admin_profile = getattr(request.user, 'admin_profile', None)
- if not admin_profile:
- return Response({
- 'code': 403,
- 'message': '管理员信息不完整',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- zhanghao = request.data.get('zhanghao')
- if not zhanghao or request.user.phone != zhanghao:
- return Response({
- 'code': 401,
- 'message': '账号验证失败',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
- # ==================== 身份验证结束 ====================
-
- # 获取业务参数
- uid = request.data.get('uid')
- user_type = request.data.get('user_type')
-
- if not uid or not user_type:
- return Response({
- 'code': 400,
- 'message': '参数缺失',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- user_type = int(user_type)
- if user_type not in [1, 2, 3, 4]:
- raise ValueError
- except ValueError:
- return Response({
- 'code': 400,
- 'message': '用户类型无效',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 查询用户主表
- user_main = UserMain.objects.filter(yonghuid=uid).first()
- if not user_main:
- return Response({
- 'code': 404,
- 'message': '用户不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 使用事务确保数据一致性
- with transaction.atomic():
- # 存储修改记录的数据
- modify_record_data = {
- 'yonghuid': uid,
- 'xiugaiid': zhanghao,
- 'leixing': user_type,
- 'qitashuoming': '',
- 'create_time': timezone.now()
- }
-
- # 标记是否有敏感字段被修改
- has_sensitive_change = False
-
- if user_type == 1: # 老板 - 不允许修改
- return Response({
- 'code': 403,
- 'message': '老板信息不允许修改',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- elif user_type == 2: # 打手
- # 获取或创建打手扩展记录
- dashou_profile, created = UserDashou.objects.get_or_create(
- user=user_main,
- defaults={'nicheng': user_main.phone or f'打手{uid}'}
- )
-
- # 记录敏感字段的旧值
- old_yue = dashou_profile.yue # 打手可提现余额
- old_zonge = dashou_profile.zonge # 打手提现金额
- old_jifen = dashou_profile.jifen # 打手积分
- old_yajin = dashou_profile.yajin # 打手押金
-
- # 需要更新的字段列表
- update_fields = [
- 'chenghao', 'dianhua', 'yue', 'zonge', 'jifen',
- 'yajin', 'yaoqingren', 'jieshao', 'zhuangtai',
- 'zaixianzhuangtai', 'zhanghaozhuangtai'
- ]
-
- # 记录被修改的字段
- modified_fields = []
-
- for field in update_fields:
- if field in request.data:
- new_value = request.data.get(field)
- old_value = getattr(dashou_profile, field)
-
- # 检查值是否改变
- if str(new_value) != str(old_value):
- # 处理特殊字段类型
- if field in ['yue', 'zonge', 'yajin']:
- new_value = Decimal(str(new_value))
- setattr(dashou_profile, field, new_value)
- elif field in ['jifen']:
- new_value = int(new_value)
- setattr(dashou_profile, field, new_value)
- elif field in ['zhuangtai', 'zaixianzhuangtai', 'zhanghaozhuangtai']:
- new_value = int(new_value)
- setattr(dashou_profile, field, new_value)
- else:
- setattr(dashou_profile, field, new_value)
-
- modified_fields.append(field)
-
- # 记录敏感字段修改
- if field == 'yue':
- has_sensitive_change = True
- modify_record_data['xiugaitijiao'] = new_value # 使用xiugaitijiao字段记录打手余额修改
- modify_record_data['xiugaitijiaoq'] = old_yue
- elif field == 'zonge':
- has_sensitive_change = True
- # zonge字段在修改记录表中可能没有直接对应,可以记录在备注中
- modify_record_data['qitashuoming'] += f' 打手总额从{old_zonge}修改为{new_value}'
- elif field == 'jifen':
- has_sensitive_change = True
- modify_record_data['jifen'] = new_value
- modify_record_data['yjifen'] = old_jifen
- elif field == 'yajin':
- has_sensitive_change = True
- modify_record_data['yajin'] = new_value
- modify_record_data['yyajin'] = old_yajin
-
- # 保存打手信息
- dashou_profile.save()
-
- # 记录被修改的非敏感字段
- non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'zonge', 'jifen', 'yajin']]
- if non_sensitive_fields:
- modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
-
- elif user_type == 3: # 管事
- # 获取或创建管事扩展记录
- guanshi_profile, created = UserGuanshi.objects.get_or_create(
- user=user_main,
- defaults={'yaoqingma': f'GS{uid[-4:]}'}
- )
-
- # 记录敏感字段的旧值
- old_yue = guanshi_profile.yue # 管事可提现余额
-
- update_fields = [
- 'yaogingshuliang', 'chongzhifenrun', 'yue',
- 'zhuangtai', 'dianhua', 'wechat'
- ]
-
- modified_fields = []
-
- for field in update_fields:
- if field in request.data:
- new_value = request.data.get(field)
- old_value = getattr(guanshi_profile, field)
-
- if str(new_value) != str(old_value):
- if field in ['chongzhifenrun', 'yue']:
- new_value = Decimal(str(new_value))
- setattr(guanshi_profile, field, new_value)
- elif field in ['yaogingshuliang']:
- new_value = int(new_value)
- setattr(guanshi_profile, field, new_value)
- elif field in ['zhuangtai']:
- new_value = int(new_value)
- setattr(guanshi_profile, field, new_value)
- else:
- setattr(guanshi_profile, field, new_value)
-
- modified_fields.append(field)
-
- # 记录敏感字段修改
- if field == 'yue':
- has_sensitive_change = True
- modify_record_data['guanshiyue'] = new_value
- modify_record_data['guanshiyueq'] = old_yue
- elif field == 'chongzhifenrun':
- has_sensitive_change = True
- modify_record_data['qitashuoming'] += f' 管事分佣从{old_value}修改为{new_value}'
-
- guanshi_profile.save()
-
- # 记录非敏感字段
- non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'chongzhifenrun']]
- if non_sensitive_fields:
- modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
-
- elif user_type == 4: # 商家
- # 获取或创建商家扩展记录
- shop_profile, created = UserShangjia.objects.get_or_create(
- user=user_main,
- defaults={'nicheng': user_main.phone or f'商家{uid}'}
- )
-
- # 记录敏感字段的旧值
- old_yue = shop_profile.yue # 商家余额
-
- update_fields = ['yue', 'zhuangtai', 'dianhua', 'wechat']
-
- modified_fields = []
-
- for field in update_fields:
- if field in request.data:
- new_value = request.data.get(field)
- old_value = getattr(shop_profile, field)
-
- if str(new_value) != str(old_value):
- if field == 'yue':
- new_value = Decimal(str(new_value))
- setattr(shop_profile, field, new_value)
- elif field == 'zhuangtai':
- new_value = int(new_value)
- setattr(shop_profile, field, new_value)
- else:
- setattr(shop_profile, field, new_value)
-
- modified_fields.append(field)
-
- # 记录敏感字段修改
- if field == 'yue':
- has_sensitive_change = True
- modify_record_data['shangjiayue'] = new_value
- modify_record_data['shangjiayueq'] = old_yue
-
- shop_profile.save()
-
- # 记录非敏感字段
- non_sensitive_fields = [f for f in modified_fields if f != 'yue']
- if non_sensitive_fields:
- modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
-
- # 处理打手会员添加(只有打手可以添加会员)
-
- # 🔥🔥🔥 后端修改:支持正负数会员天数
- # 修改会员处理逻辑部分
-
- if user_type == 2 and 'huiyuan_id' in request.data and 'days' in request.data:
- huiyuan_id = request.data.get('huiyuan_id')
- days = int(request.data.get('days', 0))
-
- # 🔥 修改:允许正负数,但不能为0
- if huiyuan_id and days != 0:
- # 🔥 修改:检查绝对值范围
- if abs(days) < 1 or abs(days) > 10000:
- return Response({
- 'code': 400,
- 'message': '会员天数必须在1-10000之间(正数增加,负数减少)',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 检查会员是否存在
- huiyuan = Huiyuan.objects.filter(huiyuan_id=huiyuan_id).first()
- if not huiyuan:
- return Response({
- 'code': 404,
- 'message': '会员不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 检查是否已购买该会员
- existing_record = Huiyuangoumai.objects.filter(
- yonghu_id=uid,
- huiyuan_id=huiyuan_id
- ).first()
-
- if existing_record:
- # 🔥🔥🔥 重要修改:支持减少天数
- # 计算新的到期时间
- new_daoqi_time = existing_record.daoqi_time + timedelta(days=days)
-
- # 🔥 安全验证:减少天数后不能小于当前时间前90天(防止过度减少)
- # 这个限制可以根据业务调整
- min_allowed_time = timezone.now() - timedelta(days=90)
- if new_daoqi_time < min_allowed_time:
- return Response({
- 'code': 400,
- 'message': f'减少天数过多,调整后到期时间不能早于 {(min_allowed_time + timedelta(days=90)).strftime("%Y-%m-%d")}',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- existing_record.daoqi_time = new_daoqi_time
- existing_record.save()
-
- # 记录会员操作
- has_sensitive_change = True
- modify_record_data['huiyuants'] = days
- modify_record_data['huiyuan_id'] = huiyuan_id
-
- if days > 0:
- modify_record_data['qitashuoming'] += f' 续费会员 {huiyuan.jieshao} {days}天'
- # 只有增加天数时才增加购买次数
- huiyuan.goumai_cishu += 1
- huiyuan.save()
- else:
- modify_record_data['qitashuoming'] += f' 减少会员 {huiyuan.jieshao} {abs(days)}天'
- # 减少天数时不增加购买次数
-
- else:
- # 🔥 修改:新购买时只能增加天数
- if days > 0:
- daoqi_time = timezone.now() + timedelta(days=days)
- new_record = Huiyuangoumai.objects.create(
- huiyuan_id=huiyuan_id,
- yonghu_id=uid,
- jieshao=huiyuan.jieshao,
- daoqi_time=daoqi_time
- )
-
- # 记录会员购买
- has_sensitive_change = True
- modify_record_data['huiyuants'] = days
- modify_record_data['huiyuan_id'] = huiyuan_id
- modify_record_data['qitashuoming'] += f' 购买会员 {huiyuan.jieshao} {days}天'
-
- # 更新会员购买次数
- huiyuan.goumai_cishu += 1
- huiyuan.save()
- else:
- # 🔥 不能为不存在的会员减少天数
- return Response({
- 'code': 400,
- 'message': '用户未购买该会员,无法减少天数',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
-
-
-
-
-
-
-
-
-
-
- # 如果有敏感字段被修改或会员操作,创建修改记录
- if has_sensitive_change or 'huiyuan_id' in request.data:
- # 创建修改记录
- Xiugaijilu.objects.create(**modify_record_data)
- elif modify_record_data.get('qitashuoming', '').strip():
- # 只有非敏感字段修改,也记录但qitashuoming不同
- Xiugaijilu.objects.create(**modify_record_data)
-
- return Response({
- 'code': 0,
- 'message': '修改成功',
- 'data': None
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- return Response({
- 'code': 500,
- 'message': f'修改失败: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-# yonghu/views.py
-from django.utils import timezone
-from django.db.models import Q
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework import status
-from .models import UserMain, AdminProfile, Tixianjilu
-
-
-# 管理员认证装饰器
-def admin_required(view_func):
- """管理员权限验证装饰器"""
-
- def wrapped_view(self, request, *args, **kwargs):
- try:
- # 1. 验证JWT Token
- if not request.user.is_authenticated:
- return Response({
- 'code': 401,
- 'message': '未认证',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 验证用户类型为管理员
- if request.user.user_type != 'admin':
- return Response({
- 'code': 403,
- 'message': '权限不足',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 3. 验证管理员扩展表存在
- admin_profile = getattr(request.user, 'admin_profile', None)
- if not admin_profile:
- return Response({
- 'code': 403,
- 'message': '管理员信息不完整',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 4. 验证管理员账号(phone)与前端传递的zhanghao一致
- zhanghao = request.data.get('zhanghao')
- if not zhanghao or request.user.phone != zhanghao:
- return Response({
- 'code': 401,
- 'message': '账号验证失败',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- return view_func(self, request, *args, **kwargs)
- except Exception as e:
- return Response({
- 'code': 500,
- 'message': f'权限验证异常: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- return wrapped_view
-
-
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
-
-from django.utils import timezone
-from django.db.models import Q
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.permissions import IsAuthenticated
-from rest_framework import status
-from yonghu.models import Tixianjilu
-
-
-class AddtxshView(APIView):
- """
- 获取提现审核列表接口
- 与用户管理页面保持一致的分页逻辑
- 支持按用户ID搜索,排序改为按申请时间升序(最早申请排最前)
- """
- permission_classes = [IsAuthenticated]
-
- @admin_required
- def post(self, request):
- try:
- # 获取参数
- zhanghao = request.data.get('zhanghao')
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 5))
- zhuangtai = int(request.data.get('zhuangtai', 1))
- leixing = int(request.data.get('leixing', 1))
- fangshi = request.data.get('fangshi')
-
- # 🔴【新增】获取搜索参数(用户ID)
- search_uid = request.data.get('search_uid', '').strip()
-
- if page < 1:
- page = 1
-
- # 计算偏移量(与用户管理页面完全一致)
- offset = (page - 1) * page_size
-
- # 构建查询条件
- query = Q()
-
- # 状态筛选
- if zhuangtai == 1:
- query &= Q(zhuangtai=1)
- elif zhuangtai == 2:
- query &= Q(zhuangtai__in=[2, 3])
-
- # 类型筛选
- query &= Q(leixing=leixing)
-
- # 收款方式筛选(可选)
- if fangshi:
- query &= Q(fangshi=int(fangshi))
-
- # 🔴【新增】如果搜索了用户ID,添加条件
- if search_uid:
- query &= Q(yonghuid=search_uid)
-
- # 🔴【修改】排序改为按申请时间升序(最早申请排最前)
- queryset = Tixianjilu.objects.filter(query).order_by('create_time')
-
- # 🔴【修改】分页逻辑:如果是搜索模式,不分页,返回所有匹配记录
- if search_uid:
- records = list(queryset)
- total_count = len(records)
- has_more = False # 搜索模式没有更多数据
- else:
- total_count = queryset.count()
- records = list(queryset[offset:offset + page_size])
- current_count = len(records)
- has_more = (offset + current_count) < total_count
-
- # 获取状态统计(统计与搜索无关,依然按筛选条件统计)
- status_counts = {
- 'awaiting': Tixianjilu.objects.filter(
- zhuangtai=1,
- leixing=leixing
- ).count(),
- 'processed': Tixianjilu.objects.filter(
- zhuangtai__in=[2, 3],
- leixing=leixing
- ).count()
- }
-
- # 构建响应数据
- withdrawal_list = []
- for record in records:
- withdrawal_list.append({
- 'id': record.id,
- 'yonghuid': record.yonghuid,
- 'avatar': record.avatar,
- 'phone': record.phone,
- 'nicheng': record.nicheng,
- 'leixing': record.leixing,
- 'zhifu': record.zhifu,
- 'skzhanghao': record.skzhanghao,
- 'jine': str(record.jine) if record.jine else '0.00',
- 'zhuangtai': record.zhuangtai,
- 'fangshi': record.fangshi,
- 'shenheid': record.shenheid,
- 'bhliyou': record.bhliyou,
- 'create_time': record.create_time,
- 'update_time': record.update_time
- })
-
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': {
- 'list': withdrawal_list,
- 'total_count': total_count,
- 'status_counts': status_counts,
- 'has_more': has_more
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- return Response({
- 'code': 500,
- 'message': f'获取提现列表失败: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from django.db import transaction
-from django.utils import timezone
-from peizhi.models import Szjilu # 添加收支记录表导入
-
-
-# 假设你的提现记录模型导入
-# from your_app.models import Tixianjilu
-
-class AdtixianqkView(APIView):
- """
- 处理提现申请接口(已更新:添加收支记录表更新)
- """
- permission_classes = [IsAuthenticated]
-
- @admin_required # 假设你有这个装饰器
- def post(self, request):
- try:
- zhanghao = request.data.get('zhanghao')
- tixian_id = request.data.get('tixian_id')
- result = int(request.data.get('result', 0))
- reason = request.data.get('reason', '')
-
- if not tixian_id:
- return Response({
- 'code': 400,
- 'message': '提现记录ID不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if result not in [2, 3]:
- return Response({
- 'code': 400,
- 'message': '操作结果无效',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 如果是拒绝,需要理由
- if result == 3 and not reason:
- return Response({
- 'code': 400,
- 'message': '拒绝提现需要提供理由',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取提现记录
- try:
- withdrawal = Tixianjilu.objects.get(id=tixian_id)
- except Tixianjilu.DoesNotExist:
- return Response({
- 'code': 404,
- 'message': '提现记录不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 检查状态,只有待审核的可以处理
- if withdrawal.zhuangtai != 1:
- return Response({
- 'code': 400,
- 'message': '该提现申请已被处理',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 使用事务确保数据一致性
- with transaction.atomic():
- # 更新提现记录
- withdrawal.zhuangtai = result
- withdrawal.shenheid = request.user.yonghuid
-
- if result == 3: # 拒绝
- withdrawal.bhliyou = reason
-
- withdrawal.update_time = timezone.now()
- withdrawal.save()
-
- # 🟢 新增:如果同意提现(result=2),更新收支记录表
- if result == 2:
- try:
- # 查询收支记录表,id=1的记录
- szjilu_record = Szjilu.objects.get(id=1)
-
- # 获取提现金额
- tixian_jine = withdrawal.jine # 假设提现记录模型中有jine字段
-
- from dingdan.utils import update_daily_payout
- update_daily_payout(tixian_jine)
-
- # 更新收支记录
- szjilu_record.zongsy -= tixian_jine # 总收益减去提现金额
- szjilu_record.zongzc += tixian_jine # 总支出加上提现金额
- szjilu_record.jrzc += tixian_jine # 今日支出加上提现金额
- szjilu_record.update_time = timezone.now()
- szjilu_record.save()
-
- except Szjilu.DoesNotExist:
- # 如果收支记录表不存在id=1的记录,创建一个
- szjilu_record = Szjilu.objects.create(
- id=1,
- zongsy=-tixian_jine if result == 2 else 0,
- zongzc=tixian_jine if result == 2 else 0,
- jrzc=tixian_jine if result == 2 else 0
- )
- except Exception as e:
- # 如果更新收支记录失败,抛出异常,事务会回滚
- raise Exception(f"更新收支记录失败: {str(e)}")
-
- return Response({
- 'code': 0,
- 'message': '处理成功',
- 'data': {
- 'id': withdrawal.id,
- 'zhuangtai': withdrawal.zhuangtai,
- 'shenheid': withdrawal.shenheid,
- 'update_time': withdrawal.update_time
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- return Response({
- 'code': 500,
- 'message': f'处理提现失败: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-# views.py (yonghu应用)
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from rest_framework import status
-from .models import UserMain, UserDashou
-import logging
-
-logger = logging.getLogger(__name__)
-
-
-class DashouZiliaoHuoquView(APIView):
- """
- 打手资料获取接口
- URL: /yonghu/dszjxxhq
- 方法: POST
- 认证: JWT Token
- 返回: 打手扩展表信息
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 使用select_related高效查询,避免N+1问题
- dashou = UserDashou.objects.select_related('user').get(user=request.user)
-
- # 构建返回数据
- data = {
- 'nicheng': dashou.nicheng if dashou.nicheng else '',
- 'jieshao': dashou.jieshao if dashou.jieshao else '',
- 'dianhua': dashou.dianhua if dashou.dianhua else '',
- 'wechat': dashou.wechat if dashou.wechat else '',
- 'uid': request.user.yonghuid, # 从主表获取UID
- 'avatar': request.user.avatar if request.user.avatar else '', # 头像相对URL
- }
-
- return Response({
- 'code': 200,
- 'msg': 'success',
- 'data': data
- }, status=status.HTTP_200_OK)
-
- except UserDashou.DoesNotExist:
- logger.warning(f'打手扩展表不存在: user_id={request.user.id}')
- return Response({
- 'code': 404,
- 'msg': '打手信息不存在'
- }, status=status.HTTP_404_NOT_FOUND)
-
- except Exception as e:
- logger.error(f'获取打手资料异常: {str(e)}', exc_info=True)
- return Response({
- 'code': 500,
- 'msg': '服务器内部错误'
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-# views.py (yonghu应用)
-from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from rest_framework import status
-from .models import UserDashou
-import re
-import logging
-
-logger = logging.getLogger(__name__)
-
-
-class DashouGengxinView(APIView):
- """
- 打手信息更新接口
- URL: /yonghu/dsgxxx
- 方法: POST
- 认证: JWT Token
- 参数: nicheng(昵称), dianhua(电话), wechat(微信), jieshao(介绍)
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 获取请求数据
- nicheng = request.data.get('nicheng', '').strip()
- dianhua = request.data.get('dianhua', '').strip()
- wechat = request.data.get('wechat', '').strip()
- jieshao = request.data.get('jieshao', '').strip()
-
- # 数据验证
- validation_errors = self.validate_data(nicheng, dianhua, wechat, jieshao)
- if validation_errors:
- return Response({
- 'code': 400,
- 'msg': validation_errors
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取打手扩展表
- try:
- dashou = UserDashou.objects.get(user=request.user)
- except UserDashou.DoesNotExist:
- return Response({
- 'code': 404,
- 'msg': '打手信息不存在'
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 更新数据
- update_fields = []
-
- if nicheng is not None:
- dashou.nicheng = nicheng if nicheng != '' else None
- update_fields.append('nicheng')
-
- if dianhua is not None:
- dashou.dianhua = dianhua if dianhua != '' else None
- update_fields.append('dianhua')
-
- if wechat is not None:
- dashou.wechat = wechat if wechat != '' else None
- update_fields.append('wechat')
-
- if jieshao is not None:
- dashou.jieshao = jieshao if jieshao != '' else None
- update_fields.append('jieshao')
-
- # 保存更新(只更新修改的字段)
- if update_fields:
- dashou.save(update_fields=update_fields)
-
- return Response({
- 'code': 200,
- 'msg': '更新成功',
- 'data': {
- 'nicheng': dashou.nicheng or '',
- 'dianhua': dashou.dianhua or '',
- 'wechat': dashou.wechat or '',
- 'jieshao': dashou.jieshao or ''
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f'更新打手信息异常: {str(e)}', exc_info=True)
- return Response({
- 'code': 500,
- 'msg': '服务器内部错误'
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def validate_data(self, nicheng, dianhua, wechat, jieshao):
- """数据验证方法"""
- errors = []
-
- # 1. 昵称验证(必填)
- if not nicheng:
- errors.append('昵称不能为空')
- elif len(nicheng) > 20:
- errors.append('昵称不能超过20个字符')
-
- # 2. 手机号验证(非必填,但填写时必须正确)
- if dianhua:
- # 手机号正则:1开头,11位数字
- phone_pattern = r'^1[3-9]\d{9}$'
- if not re.match(phone_pattern, dianhua):
- errors.append('手机号格式不正确')
-
- # 3. 微信号验证(非必填)
- if wechat and len(wechat) > 30:
- errors.append('微信号不能超过30个字符')
-
- # 4. 个人介绍验证
- if jieshao and len(jieshao) > 200:
- errors.append('个人介绍不能超过200个字符')
-
- return '; '.join(errors) if errors else None
-
-
-
-
-
-
-
-
-class ShangjiaZhuceView(APIView):
- """
- 商家注册接口 (通过管理员邀请码)
- 访问路径: /api/yonghu/shangjiahuce/
- 请求方法: POST
- 权限要求: JWT Token认证通过的用户
- 请求体: { "inviteCode": "管理员邀请码" }
-
- 返回字段说明:
- - code: 状态码(200成功,其他失败)
- - message: 提示信息
- - data: 商家信息数据,包含以下字段(与前端商家页面完全一致):
- - nicheng: 商家昵称
- - sjyue: 商家余额(字符串,保留两位小数)
- - fabu: 发布订单总数
- - chengjiao: 成交订单总数
- - tuikuan: 退款订单总数
- - jinridingdan: 今日订单
- - jinriliushui: 今日流水(字符串)
- - jinyuedingdan: 今月订单
- - jinyueliushui: 今月流水(字符串)
- - zhuangtai: 商家状态(1正常)
- - dianhua: 商家电话
- - wechat: 商家微信
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 获取并验证邀请码参数
- yaoqingma = request.data.get('inviteCode')
- if not yaoqingma:
- return Response({
- 'code': 400,
- 'message': '邀请码不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- yaoqingma = yaoqingma.strip()
- if len(yaoqingma) > 100:
- return Response({
- 'code': 400,
- 'message': '邀请码长度超过限制',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- current_user = request.user
-
- # 2. 检查用户是否已是商家
- try:
- shangjia_profile = current_user.shop_profile
- # 用户已是商家,直接返回现有信息
- return self.fanhuiXianyouShangjiaXinxi(shangjia_profile, current_user)
- except UserShangjia.DoesNotExist:
- # 用户不是商家,继续注册流程
- pass
-
- # 3. 根据邀请码查找对应的管理员
- try:
- # 从管理员表查找邀请码
- admin_profile = AdminProfile.objects.select_related('user').get(yaoqingma=yaoqingma)
- except AdminProfile.DoesNotExist:
- return Response({
- 'code': 404,
- 'message': '管理员邀请码无效或不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 核心:在数据库事务中创建商家身份
- try:
- with transaction.atomic():
- # 4.1 查询老板扩展表获取老板昵称
- boss_nickname = '普通商家'
- try:
- boss_profile = current_user.boss_profile
- if boss_profile.nickname and boss_profile.nickname.strip():
- boss_nickname = boss_profile.nickname.strip()
- except UserBoss.DoesNotExist:
- # 老板扩展表不存在,使用默认值
- pass
-
- # 4.2 创建商家扩展表
- # 使用老板昵称作为商家昵称的基础
- shangjia_nicheng = boss_nickname
-
- # 检查昵称是否已存在(安全考虑)
- if UserShangjia.objects.filter(nicheng=shangjia_nicheng).exists():
- # 如果存在,添加随机数确保唯一
- import random
- shangjia_nicheng = f"{boss_nickname}{random.randint(1000, 9999)}"
-
- # 4.3 创建商家记录
- shop_profile = UserShangjia.objects.create(
- user=current_user,
- nicheng=shangjia_nicheng,
- zhuangtai=1, # 正常状态
- yue=0.00, # 商家余额
- fabu=0, # 发布订单总数
- tuikuan=0, # 退款订单总数
- chengjiao=0, # 成交订单总数
- jinridingdan=0, # 今日订单
- jinriliushui=0.00, # 今日流水
- jinyuedingdan=0, # 今月订单
- jinyueliushui=0.00, # 今月流水
- dianhua='', # 电话
- wechat='', # 微信
- )
-
- except Exception as e:
- # 事务会自动回滚
- import traceback
- error_detail = traceback.format_exc()
- #print(f"商家注册错误详情: {error_detail}")
-
- return Response({
- 'code': 500,
- 'message': f'商家注册过程中发生系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 5. 注册成功,返回商家信息
- fanhui_data = self.zhuangbeiFanhuiShuju(shop_profile, current_user)
- return Response({
- 'code': 200,
- 'message': '商家注册成功!',
- 'data': fanhui_data
- })
-
- def fanhuiXianyouShangjiaXinxi(self, shangjia_profile, current_user):
- """处理已是商家的用户,返回其现有信息"""
- fanhui_data = self.zhuangbeiFanhuiShuju(shangjia_profile, current_user)
- return Response({
- 'code': 200,
- 'message': '您已是商家,返回当前信息。',
- 'data': fanhui_data
- })
-
- def zhuangbeiFanhuiShuju(self, shangjia_profile, current_user):
- """
- 准备返回给前端的商家数据
- 字段名必须与前端商家页面完全一致
- """
- data = {
- # 🟢 商家基本信息
- 'nicheng': shangjia_profile.nicheng, # 商家昵称
- 'sjyue': format(shangjia_profile.yue, '.2f'), # 商家余额,保留两位小数
- 'fabu': shangjia_profile.fabu, # 发布订单总数
- 'chengjiao': shangjia_profile.chengjiao, # 成交订单总数
- 'tuikuan': shangjia_profile.tuikuan, # 退款订单总数
- 'jinridingdan': shangjia_profile.jinridingdan, # 今日订单
- 'jinriliushui': format(shangjia_profile.jinriliushui, '.2f'), # 今日流水
- 'jinyuedingdan': shangjia_profile.jinyuedingdan, # 今月订单
- 'jinyueliushui': format(shangjia_profile.jinyueliushui, '.2f'), # 今月流水
- 'zhuangtai': shangjia_profile.zhuangtai, # 商家状态
- 'dianhua': shangjia_profile.dianhua or '', # 电话
- 'wechat': shangjia_profile.wechat or '', # 微信
-
- # 🟢 可能需要的主表信息
- 'uid': current_user.yonghuid, # 用户ID
- }
- return data
-
-
-
-
-
-
-
-# yonghu/ranking_views.py
-"""
-排行榜查询接口 - 管理员专用
-安全认证:JWT Token + 管理员权限验证
-"""
-
-from django.db.models import Q
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from datetime import datetime
-from .models import RankingRecord
-import logging
-
-
-
-logger = logging.getLogger(__name__)
-
-# yonghu/ranking_views.py - 修改PaihangbangGuanliQueryView的post方法参数接收
-class PaihangbangGuanliQueryView(APIView):
- """
- 排行榜查询接口 - 管理员专用
- 安全要求:
- 1. JWT Token认证
- 2. 管理员权限验证
- 3. 参数严格验证
- 4. 返回统一格式
- """
-
- # JWT Token认证
- permission_classes = [IsAuthenticated]
-
- @admin_required
- def post(self, request):
- """
- POST请求查询排行榜数据
- 参数:
- - zhanghao: 管理员账号
- - shenfen: 用户身份(必须,2=打手,3=管事,4=商家)
- - zhouqi: 周期类型(必须,1=日榜,2=月榜)
- - date: 查询日期(必须,格式:YYYY-MM-DD)
-
- 返回格式:
- {
- "code": 0, # 0=成功,非0=失败
- "message": "成功",
- "data": {
- "list": [...], # 排行榜数据列表
- "query_info": {...} # 查询信息
- }
- }
- """
- try:
- # 1. 获取请求数据
- data = request.data
- zhanghao = data.get('zhanghao')
- shenfen = data.get('shenfen')
- zhouqi = data.get('zhouqi')
- date_str = data.get('date')
-
- # 2. 参数验证
- if not zhanghao:
- return Response({
- 'code': 400,
- 'message': '管理员账号不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not shenfen:
- return Response({
- 'code': 400,
- 'message': '用户身份不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not zhouqi:
- return Response({
- 'code': 400,
- 'message': '周期类型不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not date_str:
- return Response({
- 'code': 400,
- 'message': '查询日期不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 参数类型和范围验证
- try:
- zhouqi_int = int(zhouqi)
- shenfen_int = int(shenfen)
- date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
- except ValueError as e:
- return Response({
- 'code': 400,
- 'message': f'参数格式错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证周期类型范围
- if zhouqi_int not in [1, 2]:
- return Response({
- 'code': 400,
- 'message': '周期类型必须为1(日榜)或2(月榜)',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证用户身份范围
- if shenfen_int not in [2, 3, 4]:
- return Response({
- 'code': 400,
- 'message': '用户身份必须为2(打手)、3(管事)或4(商家)',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 构建查询条件
- query = Q(
- zhouqi=zhouqi_int,
- tongji_date=date_obj,
- shenfen=shenfen_int
- )
-
- # 5. 执行查询(按排名升序)
- records = RankingRecord.objects.filter(query).order_by('paiming')
-
- # 6. 格式化返回数据
- data_list = []
- for record in records:
- # 根据用户身份确定指标值单位
- zhizhi_danwei = self._get_zhizhi_danwei(shenfen_int)
-
- data_list.append({
- 'paiming': record.paiming,
- 'yonghuid': record.yonghuid,
- 'nicheng': record.nicheng,
- 'avatar': record.avatar or '',
- 'zhizhi': float(record.zhizhi),
- 'zhizhi_danwei': zhizhi_danwei,
- })
-
- # 7. 返回成功响应
- response_data = {
- 'code': 0,
- 'message': '查询成功',
- 'data': {
- 'list': data_list,
- 'total_count': len(data_list),
- 'query_info': {
- 'zhouqi': zhouqi_int,
- 'zhouqi_text': '日榜' if zhouqi_int == 1 else '月榜',
- 'shenfen': shenfen_int,
- 'shenfen_text': self._get_shenfen_text(shenfen_int),
- 'date': date_str,
- }
- }
- }
-
- return Response(response_data, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f"管理员查询排行榜失败: {str(e)}", exc_info=True)
- return Response({
- 'code': 500,
- 'message': f'查询排行榜失败: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def _get_shenfen_text(self, shenfen):
- """获取身份文本"""
- shenfen_map = {2: '打手', 3: '管事', 4: '商家'}
- return shenfen_map.get(shenfen, '未知')
-
- def _get_zhizhi_danwei(self, shenfen):
- """获取指标值单位"""
- danwei_map = {2: '元', 3: '人', 4: '元'}
- return danwei_map.get(shenfen, '')
-
-
-
-
-
-
-
-# yonghu/ranking_views.py - 添加以下代码
-class PaihangbangRiqiliebiaoView(APIView):
- """
- 获取排行榜可查询日期列表 - 管理员专用
- 返回当前排行榜表中存在的日期(日榜和月榜分开)
- """
-
- # JWT Token认证
- permission_classes = [IsAuthenticated]
-
- @admin_required
- def post(self, request):
- """
- POST请求获取可查询日期列表
- 参数:
- - zhanghao: 管理员账号(用于验证)
-
- 返回格式:
- {
- "code": 0,
- "message": "成功",
- "data": {
- "ribang_dates": ["2024-01-15", "2024-01-14", ...], # 日榜日期列表
- "yuebang_dates": ["2024-01-01", "2023-12-01", ...], # 月榜日期列表
- }
- }
- """
- try:
- # 1. 获取请求数据
- data = request.data
- zhanghao = data.get('zhanghao')
-
- if not zhanghao:
- return Response({
- 'code': 400,
- 'message': '管理员账号不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 查询日榜日期(最近3个月内的)
- three_months_ago = timezone.now().date() - timedelta(days=90)
-
- ribang_dates = RankingRecord.objects.filter(
- zhouqi=1, # 日榜
- tongji_date__gte=three_months_ago # 只查询最近3个月
- ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date')
-
- # 3. 查询月榜日期(最近12个月内的)
- twelve_months_ago = timezone.now().date() - timedelta(days=365)
-
- yuebang_dates = RankingRecord.objects.filter(
- zhouqi=2, # 月榜
- tongji_date__gte=twelve_months_ago # 只查询最近12个月
- ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date')
-
- # 4. 格式化日期
- ribang_date_list = [date.strftime('%Y-%m-%d') for date in ribang_dates]
- yuebang_date_list = [date.strftime('%Y-%m-%d') for date in yuebang_dates]
-
- # 5. 返回成功响应
- response_data = {
- 'code': 0,
- 'message': '获取成功',
- 'data': {
- 'ribang_dates': ribang_date_list,
- 'yuebang_dates': yuebang_date_list,
- 'ribang_count': len(ribang_date_list),
- 'yuebang_count': len(yuebang_date_list),
- }
- }
-
- return Response(response_data, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f"获取排行榜日期列表失败: {str(e)}", exc_info=True)
- return Response({
- 'code': 500,
- 'message': f'获取日期列表失败: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-
-
-
-
-class WechatLoginAndDashouRegisterView(APIView):
- """
- 微信登录并注册打手一体化接口
- 访问路径: /api/yonghu/wdlyhdl
- 请求方法: POST
- 权限要求: 无需登录
- 请求体: { "code": "微信登录code", "inviteCode": "邀请码" }
-
- 功能: 用户扫码进入但未登录时,一次性完成微信登录和打手注册
- """
-
- throttle_classes = [AnonRateThrottle]
- permission_classes = [AllowAny]
-
- def post(self, request):
- """
- 处理微信登录并注册打手请求
- """
- try:
- # 1. 获取并验证参数
- code = request.data.get('code', '').strip()
- yaoqingma = request.data.get('inviteCode', '').strip()
-
- if not code:
- return Response({
- 'code': 1,
- 'msg': '微信授权码不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not yaoqingma:
- return Response({
- 'code': 2,
- 'msg': '邀请码不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if len(yaoqingma) > 100:
- return Response({
- 'code': 3,
- 'msg': '邀请码长度超过限制',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 获取微信openid
- wechat_data = self.get_wechat_openid(code)
- if not wechat_data or 'openid' not in wechat_data:
- error_msg = wechat_data.get('errmsg', '微信授权失败')
- return Response({
- 'code': 4,
- 'msg': f'微信登录失败: {error_msg}',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- openid = wechat_data['openid']
-
- # 3. 获取客户端真实IP
- kehuduan_ip = self.huoquKehuduanIP(request)
-
- # 4. 开始数据库事务(确保登录和注册的原子性)
- with transaction.atomic():
- # 4.1 创建或获取用户(登录逻辑)
- user_main, created = UserMain.objects.select_for_update().get_or_create(
- openid=openid,
- defaults={
- 'yonghuid': self.shengchengYonghuID(),
- 'user_type': 'normal',
- }
- )
-
- # 更新用户IP和最后登录时间
- cunchu_ip = kehuduan_ip
- user_main.ip = cunchu_ip
- user_main.last_login_time = timezone.now()
- user_main.save()
-
- if created:
- # 新用户:创建老板扩展表
- UserBoss.objects.create(user=user_main, nickname='微信用户')
-
- # 4.2 验证邀请码对应的管事(注册逻辑)
- try:
- # 使用select_related获取管事及其关联的主表用户信息
- guanshi_profile = UserGuanshi.objects.select_related('user').get(yaoqingma=yaoqingma)
- except UserGuanshi.DoesNotExist:
- # 邀请码无效,但用户已登录,可以返回登录成功但注册失败
- # 这里我们继续执行,但只返回登录信息
- return self.fanhuiZhihouDengluXinxi(user_main, yaoqingma_wuxiao=True)
-
- # 验证管事状态
-
- # 4.3 检查用户是否已是打手
- dashou_exists = UserDashou.objects.filter(user=user_main).exists()
- if dashou_exists:
- # 用户已是打手,直接返回信息
- dashou_profile = UserDashou.objects.get(user=user_main)
- return self.fanhuiDengluZhuceshuju(user_main, dashou_profile)
-
- # 4.4 核心:为用户创建所有身份(打手、商家、管事)
- guanshi_yonghuid = guanshi_profile.user.yonghuid
-
- # 创建打手扩展表
- dashou_profile = UserDashou.objects.create(
- user=user_main,
- nicheng='大神',
- chenghao='普通大神',
- yaoqingren=guanshi_yonghuid,
- # 其他字段使用模型默认值
- )
-
- # 创建商家扩展表
-
- # 创建管事扩展表
-
- # 更新原管事的邀请数量
- locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
- locked_guanshi.yaogingshuliang += 1
- locked_guanshi.save()
-
- # ========== 新增:管事每日统计(邀请打手) ==========
- try:
- import logging
-
- logger = logging.getLogger(__name__)
- from houtai.utils import update_guanshi_daily_by_action
- update_guanshi_daily_by_action(
- yonghuid=guanshi_yonghuid,
- action=1, # 1 = 邀请打手
- amount=Decimal('0.00')
- )
- logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}")
- except Exception as e:
- logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}")
-
- # 5. 生成JWT token
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
-
- # 6. 准备返回数据
- response_data = self.zhuangbeiFanhuiShuju(user_main, dashou_profile, token)
-
- return Response({
- 'code': 0,
- 'msg': '登录并注册成功!已开通打手、管事等身份。',
- 'data': response_data
- })
-
- except Exception as e:
- # 记录错误日志
- import logging
- logger = logging.getLogger(__name__)
- error_openid = locals().get('openid', 'N/A')
- logger.error(
- f"微信登录注册一体化接口异常 - openid: {error_openid}, "
- f"错误类型: {type(e).__name__}, 错误详情: {str(e)}",
- exc_info=True
- )
-
- # 返回友好错误信息
- return Response({
- 'code': 99,
- 'msg': '系统繁忙,请稍后重试',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def get_wechat_openid(self, code):
- """
- 调用微信接口获取openid
- """
- try:
- appid = getattr(settings, 'WEIXIN_APPID', '')
- secret = getattr(settings, 'WEIXIN_SECRET', '')
-
- if not appid or not secret:
- raise ValueError('微信配置未设置')
-
- url = 'https://api.weixin.qq.com/sns/jscode2session'
- params = {
- 'appid': appid,
- 'secret': secret,
- 'js_code': code,
- 'grant_type': 'authorization_code'
- }
-
- response = requests.get(url, params=params, timeout=10)
- result = response.json()
-
- if 'openid' in result:
- return result
- else:
- errcode = result.get('errcode', 'unknown')
- errmsg = result.get('errmsg', '未知错误')
- return {'errmsg': f'[{errcode}]{errmsg}'}
-
- except requests.exceptions.Timeout:
- return {'errmsg': '请求微信接口超时'}
- except Exception as e:
- return {'errmsg': f'请求微信接口异常: {str(e)}'}
-
- def shengchengYonghuID(self):
- """
- 生成7位数字用户ID
- """
- for _ in range(10):
- timestamp_part = str(int(time.time()))[-5:].zfill(5)
- random_part = str(random.randint(0, 99)).zfill(2)
- user_id = timestamp_part + random_part
-
- if len(user_id) == 7 and user_id.isdigit():
- if not UserMain.objects.filter(yonghuid=user_id).exists():
- return user_id
-
- raise Exception('生成用户ID失败')
-
- def huoquKehuduanIP(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
-
- ip = request.META.get('REMOTE_ADDR', '')
- return ip if ip else '0.0.0.0'
-
- def huoquQunPeizhi(self):
- """
- 获取群配置信息
- """
- try:
- qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2])
-
- result = {
- 'dashouqun': '',
- 'dashouqunid': '',
- 'guanshiqun': '',
- 'guanshiqunid': ''
- }
-
- for config in qun_configs:
- if config.id == 1:
- result['dashouqun'] = config.neirong or ''
- result['dashouqunid'] = config.qunid or ''
- elif config.id == 2:
- result['guanshiqun'] = config.neirong or ''
- result['guanshiqunid'] = config.qunid or ''
-
- return result
-
- except Exception:
- return {
- 'dashouqun': '',
- 'dashouqunid': '',
- 'guanshiqun': '',
- 'guanshiqunid': ''
- }
-
- def fanhuiZhihouDengluXinxi(self, user_main, yaoqingma_wuxiao=False, guanshi_jinyong=False):
- """
- 返回只登录成功的信息(当邀请码无效或管事禁用时)
- """
- # 获取老板昵称
- boss_nickname = '微信用户'
- try:
- boss_profile = UserBoss.objects.get(user=user_main)
- if boss_profile.nickname:
- boss_nickname = boss_profile.nickname
- except UserBoss.DoesNotExist:
- pass
-
- # 检查各身份状态
- dashou_status = 1 if UserDashou.objects.filter(user=user_main).exists() else 0
- shangjia_status = 1 if UserShangjia.objects.filter(user=user_main).exists() else 0
- guanshi_status = 1 if UserGuanshi.objects.filter(user=user_main).exists() else 0
-
- # 生成token
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
-
- # 获取群配置
- group_info = self.huoquQunPeizhi()
-
- # 构建返回数据
- response_data = {
- 'token': token,
- 'nicheng': boss_nickname,
- 'uid': user_main.yonghuid,
- 'touxiang': user_main.avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- # 🟢 注意:这里没有返回dingdantiaoshu(订单数量),与前端需求一致
- 'dashouqun': group_info.get('dashouqun', ''),
- 'dashouqunid': group_info.get('dashouqunid', ''),
- 'guanshiqun': group_info.get('guanshiqun', ''),
- 'guanshiqunid': group_info.get('guanshiqunid', '')
- }
-
- # 根据情况设置返回消息
- if yaoqingma_wuxiao:
- msg = '登录成功,但邀请码无效'
- elif guanshi_jinyong:
- msg = '登录成功,但邀请码对应的管事账号已被禁用'
- else:
- msg = '登录成功'
-
- return Response({
- 'code': 0,
- 'msg': msg,
- 'data': response_data
- })
-
- def fanhuiDengluZhuceshuju(self, user_main, dashou_profile):
- """
- 返回登录并注册的数据(用户已是打手的情况)
- """
- # 获取老板昵称
- boss_nickname = '微信用户'
- try:
- boss_profile = UserBoss.objects.get(user=user_main)
- if boss_profile.nickname:
- boss_nickname = boss_profile.nickname
- except UserBoss.DoesNotExist:
- pass
-
- # 检查各身份状态(这里用户已是打手,所以都是1)
- dashou_status = 1
- shangjia_status = 1 if UserShangjia.objects.filter(user=user_main).exists() else 1 # 确保是1
- guanshi_status = 1 if UserGuanshi.objects.filter(user=user_main).exists() else 1 # 确保是1
-
- # 生成token
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
-
- # 获取群配置
- group_info = self.huoquQunPeizhi()
-
- # 准备打手信息
- dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile)
-
- # 合并所有数据
- response_data = {
- 'token': token,
- 'nicheng': boss_nickname,
- 'uid': user_main.yonghuid,
- 'touxiang': user_main.avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- 'dashouqun': group_info.get('dashouqun', ''),
- 'dashouqunid': group_info.get('dashouqunid', ''),
- 'guanshiqun': group_info.get('guanshiqun', ''),
- 'guanshiqunid': group_info.get('guanshiqunid', ''),
- # 合并打手信息
- **dashou_data
- }
-
- return Response({
- 'code': 0,
- 'msg': '您已是打手,登录成功',
- 'data': response_data
- })
-
- def zhuangbeiFanhuiShuju(self, user_main, dashou_profile, token):
- """
- 准备完整的返回数据(登录+注册成功的情况)
- """
- # 获取老板昵称
- boss_nickname = '微信用户'
- try:
- boss_profile = UserBoss.objects.get(user=user_main)
- if boss_profile.nickname:
- boss_nickname = boss_profile.nickname
- except UserBoss.DoesNotExist:
- pass
-
- # 身份状态(刚注册成功,应该都是1)
- dashou_status = 1
- shangjia_status = 1
- guanshi_status = 1
-
- # 获取群配置
- group_info = self.huoquQunPeizhi()
-
- # 准备打手信息
- dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile)
-
- # 合并所有数据
- response_data = {
- 'token': token,
- 'nicheng': boss_nickname,
- 'uid': user_main.yonghuid,
- 'touxiang': user_main.avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- 'dashouqun': group_info.get('dashouqun', ''),
- 'dashouqunid': group_info.get('dashouqunid', ''),
- 'guanshiqun': group_info.get('guanshiqun', ''),
- 'guanshiqunid': group_info.get('guanshiqunid', ''),
- # 合并打手信息
- **dashou_data
- }
-
- return response_data
-
- def zhuangbeiDashouFanhuiShuju(self, dashou_profile):
- """
- 准备打手返回数据(字段名严格匹配前端)
- """
- # 构建基础信息
- data = {
- 'dashounicheng': dashou_profile.nicheng,
- 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
- 'yongjin': str(dashou_profile.yue), # Decimal转字符串
- 'zonge': str(dashou_profile.zonge),
- 'yajin': str(dashou_profile.yajin),
- 'chenghao': dashou_profile.chenghao,
- 'jinfen': dashou_profile.jifen,
- 'chengjiaoliang': dashou_profile.chengjiaozongliang,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'dashouzhuangtai': dashou_profile.zhuangtai,
- }
-
- # 查询并处理会员列表 (clumber)
- # 使用当前用户的yonghuid进行查询
- huiyuan_goumai_list = []
- goumai_records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid)
- for record in goumai_records:
- # 调用方法检查是否到期,此方法内部会更新状态
- shifou_daoqi = record.jiance_shifou_daoqi()
- huiyuan_goumai_list.append({
- 'huiyuan_id': record.huiyuan_id,
- 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
- 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
- 'shifou_daoqi': shifou_daoqi,
- 'jieshao': record.jieshao
- })
-
- data['clumber'] = huiyuan_goumai_list
- return data
-
-from datetime import datetime # 🔴【关键修改】
-class YonghuTixianShenheXiugaiView(APIView):
- """
- 用户修改审核中提现申请接口
- 访问路径: /api/yonghu/yonghutxshxg
- 请求方法: POST
- 权限要求: JWT Token认证通过的用户
- 请求体: multipart/form-data 格式
- 支持字段: tixian_id, fangshi, txdianhua, txzh, file (图片文件)
-
- 功能: 允许用户修改审核中提现申请的收款方式、电话、账号、收款码等
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = (MultiPartParser, FormParser, JSONParser)
-
- def post(self, request):
- """
- 处理用户修改审核中提现申请的POST请求
- """
- try:
- # 1. 获取当前用户
- user_main = request.user
- current_yonghuid = user_main.yonghuid
-
- # 2. 验证必填字段:提现记录ID
- tixian_id = request.data.get('tixian_id')
- if not tixian_id:
- return Response({
- 'code': 1,
- 'msg': '提现记录ID不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 查询提现记录(使用select_for_update确保事务安全)
- with transaction.atomic():
- try:
- tixian_record = Tixianjilu.objects.select_for_update().get(
- id=tixian_id,
- yonghuid=current_yonghuid # 确保只能修改自己的记录
- )
- except Tixianjilu.DoesNotExist:
- return Response({
- 'code': 2,
- 'msg': '提现记录不存在或无权修改',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 验证提现记录状态(必须是审核中)
- if tixian_record.zhuangtai != 1:
- return Response({
- 'code': 3,
- 'msg': '只有审核中的提现申请可以修改',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 获取并验证提现方式
- fangshi = request.data.get('fangshi')
- if fangshi:
- fangshi = int(fangshi)
- if fangshi not in [1, 2]:
- return Response({
- 'code': 4,
- 'msg': '提现方式无效,请选择1(微信)或2(支付宝)',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 检查提现类型与方式的匹配(如果前端提供了leixing)
- leixing = request.data.get('leixing')
- if leixing:
- leixing = int(leixing)
- # 这里可以根据业务逻辑添加类型与方式的验证
- # 例如:某些类型只能使用某些方式
-
- # 6. 验证手机号(必填)
- txdianhua = request.data.get('txdianhua', '').strip()
- if not txdianhua:
- return Response({
- 'code': 5,
- 'msg': '收款人电话不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证手机号格式
- if not txdianhua.isdigit() or len(txdianhua) != 11:
- return Response({
- 'code': 6,
- 'msg': '请输入11位有效的手机号码',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 7. 获取收款账号(可选)
- txzh = request.data.get('txzh', '').strip()
-
- # 8. 处理收款码图片上传(如果有)
- txtupian_file = request.FILES.get('file')
- new_zhifu_url = None
- old_zhifu_url = tixian_record.zhifu
-
- if txtupian_file:
- # 验证图片
- is_valid, error_msg = validate_image(txtupian_file)
- if not is_valid:
- return Response({
- 'code': 7,
- 'msg': f'图片验证失败: {error_msg}',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 上传收款码图片
- new_zhifu_url = self.handle_shoukuanma_upload(
- user_main,
- txtupian_file,
- old_zhifu_url,
- tixian_record.leixing
- )
-
- if not new_zhifu_url:
- return Response({
- 'code': 8,
- 'msg': '收款码上传失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 9. 验证收款信息(账号和图片至少有一个)
- if not txzh and not (new_zhifu_url or old_zhifu_url):
- return Response({
- 'code': 9,
- 'msg': '请填写收款账号或上传收款码',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 10. 更新提现记录
- update_fields = []
-
- # 更新提现方式(如果有)
- if fangshi:
- tixian_record.fangshi = fangshi
- update_fields.append('fangshi')
-
- # 更新手机号(必填)
- tixian_record.phone = txdianhua
- update_fields.append('phone')
-
- # 更新收款账号(如果有)
- if txzh is not None: # 注意:空字符串也要更新
- tixian_record.skzhanghao = txzh
- update_fields.append('skzhanghao')
-
- # 更新收款码图片(如果有新图片)
- if new_zhifu_url:
- tixian_record.zhifu = new_zhifu_url
- update_fields.append('zhifu')
-
- # 保存更新
- if update_fields:
- tixian_record.save(update_fields=update_fields)
-
- # 11. 构建返回数据
- response_data = {
- 'tixian_id': tixian_record.id,
- 'fangshi': tixian_record.fangshi,
- 'txdianhua': tixian_record.phone,
- 'txzh': tixian_record.skzhanghao or '',
- 'txtupian': tixian_record.zhifu or '' # 返回相对URL
- }
-
- # 12. 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '提现申请修改成功',
- 'data': response_data
- })
-
- except Exception as e:
- # 记录详细错误日志
- import logging
- logger = logging.getLogger(__name__)
- logger.error(
- f"修改提现申请异常 - 用户ID: {getattr(user_main, 'yonghuid', 'N/A')}, "
- f"提现ID: {tixian_id}, "
- f"错误类型: {type(e).__name__}, "
- f"错误详情: {str(e)}",
- exc_info=True
- )
-
- # 返回友好错误信息
- return Response({
- 'code': 99,
- 'msg': '系统繁忙,请稍后重试',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def handle_shoukuanma_upload(self, user_main, image_file, old_zhifu_url, leixing=None):
- """
- 处理收款码图片上传到腾讯云COS
- 存储路径: tixian/{yonghuid}/{timestamp_uuid}.扩展名
-
- 参数:
- - user_main: 当前用户对象
- - image_file: 图片文件对象
- - old_zhifu_url: 旧收款码URL(用于删除)
- - leixing: 提现类型(可选,用于文件夹分类)
-
- 返回: 新的图片相对路径
- """
- try:
- yonghuid = user_main.yonghuid
- if not yonghuid:
- return None
-
- # 删除旧收款码(如果存在且不是空字符串)
- if old_zhifu_url and old_zhifu_url.strip():
- delete_from_oss(old_zhifu_url)
-
- # 获取文件扩展名
- file_name = image_file.name
- file_extension = os.path.splitext(file_name)[1].lower()
- if not file_extension:
- file_extension = '.jpg'
-
- # 生成唯一文件名(使用时间戳+UUID确保不重复)
- timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
- unique_id = str(uuid.uuid4())[:8] # 取UUID前8位
- unique_file_name = f"{timestamp}_{unique_id}{file_extension}"
-
- # 根据提现类型构建文件夹路径(如果提供了leixing)
- if leixing:
- # leixing=1:打手佣金, leixing=2:管事分红
- type_folder = 'dashou' if leixing == 1 else 'guanshi'
- oss_file_path = f"tixian/{type_folder}/{yonghuid}/{unique_file_name}"
- else:
- # 默认路径
- oss_file_path = f"tixian/{yonghuid}/{unique_file_name}"
-
- # 上传新收款码到OSS
- new_shoukuanma_url = upload_to_oss(image_file, oss_file_path)
-
- if new_shoukuanma_url:
- # 返回相对路径(从tixian文件夹开始)
- return oss_file_path
- else:
- return None
-
- except Exception as e:
- print(f"处理收款码上传失败: {e}")
- return None
-
-
-# views.py
-from django.db.models import Q, Prefetch
-from django.db import connection
-from django.core.paginator import Paginator
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from dingdan.models import Chufajilu, Chufatupian
-from yonghu.models import UserDashou
-import logging
-
-logger = logging.getLogger(__name__)
-
-
-class DashouPermission:
- """
- 打手权限验证类
- 确保用户是打手且账号状态正常
- """
-
- def has_permission(self, request, view):
- user = request.user
-
- # 检查用户是否已认证
- if not user.is_authenticated:
- return False
-
- try:
- # 通过反向关系获取打手扩展信息
- dashou_profile = user.dashou_profile
-
- # 检查账号状态是否为1(正常)
- if dashou_profile.zhanghaozhuangtai != 1:
- return False
-
- return True
- except UserDashou.DoesNotExist:
- return False
- except Exception as e:
- logger.error(f"打手权限验证异常: {e}")
- return False
-
-
-from django.db.models import Q, Count, Case, When, IntegerField
-
-
-
-logger = logging.getLogger(__name__)
-
-
-class ChufaJiluHuoquView(APIView):
- """
- 打手获取处罚记录接口
- 路径: /yonghu/dshqcfjl
- 方法: POST
- 权限: JWT Token + 打手权限验证
- """
- permission_classes = [IsAuthenticated, DashouPermission]
-
- def post(self, request):
- try:
- # 获取当前用户信息
- user_main = request.user
- yonghuid = user_main.yonghuid
-
- # 通过反向关系获取打手扩展信息
- try:
- dashou_profile = user_main.dashou_profile
- # 再次确认账号状态
- if dashou_profile.zhanghaozhuangtai != 1:
- return Response({
- 'code': 3,
- 'msg': '账号已被封禁,无法查看处罚记录',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
- except UserDashou.DoesNotExist:
- return Response({
- 'code': 2,
- 'msg': '用户不是打手身份',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 判断请求类型:统计信息 or 记录列表
- qingqiu_tongji = request.data.get('qingqiu_tongji', False)
-
- if qingqiu_tongji:
- # 返回统计信息
- return self.get_tongji_info(yonghuid)
- else:
- # 返回处罚记录列表
- return self.get_chufa_list(yonghuid, request.data)
-
- except Exception as e:
- logger.error(f"获取处罚记录异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
- return Response({
- 'code': 99,
- 'msg': '系统繁忙,请稍后重试',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def get_tongji_info(self, yonghuid):
- """
- 获取统计信息(总记录、待处理、已处理)
- """
- try:
- # 使用单个查询获取所有统计信息
- stats = Chufajilu.objects.filter(dashouid=yonghuid).aggregate(
- # 总记录数
- zongshu=Count('id'),
-
- # 待处理数:状态为0(待处罚)和3(申诉中)
- daichuli=Count(
- Case(
- When(Q(sqzhuangtai=0) | Q(sqzhuangtai=3), then=1),
- output_field=IntegerField()
- )
- ),
-
- # 已处理数:状态为1(已处罚)和2(已驳回)
- yichuli=Count(
- Case(
- When(Q(sqzhuangtai=1) | Q(sqzhuangtai=2), then=1),
- output_field=IntegerField()
- )
- )
- )
-
- # 如果查询结果为None,则设置为0
- tongji_data = {
- 'zongshu': stats.get('zongshu', 0) or 0,
- 'daichuli': stats.get('daichuli', 0) or 0,
- 'yichuli': stats.get('yichuli', 0) or 0
- }
-
- return Response({
- 'code': 0,
- 'msg': '获取统计信息成功',
- 'data': tongji_data
- })
-
- except Exception as e:
- logger.error(f"获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}")
- return Response({
- 'code': 1,
- 'msg': '获取统计信息失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def get_chufa_list(self, yonghuid, request_data):
- """
- 获取处罚记录列表(高性能批量查询)
- 🔴 只修改图片查询逻辑,字段保持原样
- """
- try:
- # 获取分页参数
- page = int(request_data.get('page', 1))
- page_size = int(request_data.get('page_size', 10))
-
- # 限制最大每页数量,防止恶意请求
- if page_size > 50:
- page_size = 50
- if page_size < 1:
- page_size = 10
-
- # 获取状态筛选参数
- zhuangtai = request_data.get('zhuangtai', 0)
-
- # 🔴 构建查询条件 - 打手端根据dashouid查询
- queryset = Chufajilu.objects.filter(dashouid=yonghuid)
-
- if zhuangtai == 0:
- # 待处理:状态为0(待处罚)和3(申诉中)
- queryset = queryset.filter(Q(sqzhuangtai=0) | Q(sqzhuangtai=3))
- elif zhuangtai == 1:
- # 已处理:状态为1(已处罚)和2(已驳回)
- queryset = queryset.filter(Q(sqzhuangtai=1) | Q(sqzhuangtai=2))
- else:
- # 默认返回所有状态
- pass
-
- # 按创建时间倒序排列(最新在前)
- queryset = queryset.order_by('-create_time')
-
- # 使用游标分页,避免传统分页的性能问题
- paginator = Paginator(queryset, page_size)
-
- try:
- page_obj = paginator.page(page)
- except:
- # 页码超出范围,返回空列表
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': {
- 'list': [],
- 'has_more': False,
- 'page': page,
- 'page_size': page_size,
- 'current_count': 0
- }
- })
-
- # 获取当前页的记录
- chufa_records = list(page_obj)
-
- if not chufa_records:
- # 没有数据直接返回
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': {
- 'list': [],
- 'has_more': False,
- 'page': page,
- 'page_size': page_size,
- 'current_count': 0
- }
- })
-
- # 🔴【核心修复】收集所有需要查询图片的组合
- dingdan_ids = []
- shangjia_ids = []
-
- for record in chufa_records:
- if record.dingdan_id:
- dingdan_ids.append(record.dingdan_id)
- if record.qingqiuid: # 商家ID
- shangjia_ids.append(record.qingqiuid)
-
- # 去重
- dingdan_ids = list(set(dingdan_ids))
- shangjia_ids = list(set(shangjia_ids))
-
- # 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误)
- # 1. 查询证据图片(商家上传的)
- zhengju_mapping = {}
- if dingdan_ids and shangjia_ids:
- # 构建Q对象:dingdan_id在列表中 AND yonghuid在商家ID列表中
- zhengju_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=shangjia_ids)
- zhengju_queryset = Chufatupian.objects.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian')
-
- for item in zhengju_queryset:
- key = f"{item['dingdan_id']}_{item['yonghuid']}"
- if key not in zhengju_mapping:
- zhengju_mapping[key] = []
- if item['tupian']:
- zhengju_mapping[key].append(item['tupian'])
-
- # 2. 查询申诉图片(打手上传的)
- shensu_mapping = {}
- if dingdan_ids:
- # 构建Q对象:dingdan_id在列表中 AND yonghuid=当前打手ID
- shensu_q = Q(dingdan_id__in=dingdan_ids, yonghuid=yonghuid)
- shensu_queryset = Chufatupian.objects.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian')
-
- for item in shensu_queryset:
- key = f"{item['dingdan_id']}_{item['yonghuid']}"
- if key not in shensu_mapping:
- shensu_mapping[key] = []
- if item['tupian']:
- shensu_mapping[key].append(item['tupian'])
-
- # 🔴【核心修复】组装返回数据 - 字段保持原样!
- chufa_list = []
- for record in chufa_records:
- dingdan_id = record.dingdan_id
- shangjia_id = record.qingqiuid
-
- # 获取证据图片(商家上传的)
- zhengju_tupian = []
- if dingdan_id and shangjia_id:
- key = f"{dingdan_id}_{shangjia_id}"
- zhengju_tupian = zhengju_mapping.get(key, [])
-
- # 获取申诉图片(打手上传的)
- shensu_tupian = []
- if dingdan_id:
- key = f"{dingdan_id}_{yonghuid}"
- shensu_tupian = shensu_mapping.get(key, [])
-
- # 🔴【字段保持原样!】与前端完全对应!
- chufa_data = {
- 'id': record.id,
- 'dingdan_id': record.dingdan_id or '',
- 'qingqiuid': record.qingqiuid or '', # 商家ID
- 'cfliyou': record.cfliyou or '',
- 'sqzhuangtai': record.sqzhuangtai or 0,
- 'ssliyou': record.ssliyou or '',
- 'jifen': record.jifen or 0,
- 'create_time': record.create_time.strftime('%Y-%m-%d %H:%M:%S') if record.create_time else '',
- 'update_time': record.update_time.strftime('%Y-%m-%d %H:%M:%S') if record.update_time else '',
- 'zhengju_tupian': zhengju_tupian, # 商家上传的证据图片
- 'shensu_tupian': shensu_tupian, # 打手上传的申诉图片
- }
- chufa_list.append(chufa_data)
-
- # 判断是否还有更多数据
- has_more = page_obj.has_next()
-
- # 构建响应数据
- response_data = {
- 'list': chufa_list,
- 'has_more': has_more,
- 'page': page,
- 'page_size': page_size,
- 'current_count': len(chufa_list)
- }
-
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': response_data
- })
-
- except ValueError as e:
- logger.error(f"参数错误 - 用户ID: {yonghuid}, 错误: {str(e)}")
- return Response({
- 'code': 4,
- 'msg': '参数格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- logger.error(f"获取处罚记录列表异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
- return Response({
- 'code': 5,
- 'msg': '获取记录列表失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-class DashouShensuView(APIView):
- """
- 打手申诉接口
- 路径: /yonghu/dscfss
- 方法: POST
- 权限: JWT Token + 打手权限验证
- 功能: 打手对处罚进行申诉
- """
- permission_classes = [IsAuthenticated, DashouPermission]
-
- def post(self, request):
- try:
- # 获取当前用户信息
- user_main = request.user
- yonghuid = user_main.yonghuid
-
- # 🔴🆕【关键】验证打手账号状态
- try:
- dashou_profile = user_main.dashou_profile
- if dashou_profile.zhanghaozhuangtai != 1:
- return Response({
- 'code': 3,
- 'msg': '账号已被封禁,无法申诉',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
- except UserDashou.DoesNotExist:
- return Response({
- 'code': 2,
- 'msg': '用户不是打手身份',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 🔴🆕【关键】获取请求参数
- chufa_id = request.data.get('chufa_id') # 处罚记录ID
- shensu_liyou = request.data.get('shensu_liyou', '').strip() # 申诉理由
- shensu_tupian_urls = request.data.get('shensu_tupian_urls', []) # 申诉图片URL数组
-
- # 验证必填字段
- if not chufa_id:
- return Response({
- 'code': 6,
- 'msg': '处罚记录ID不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not shensu_liyou:
- return Response({
- 'code': 7,
- 'msg': '申诉理由不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证申诉理由长度
- if len(shensu_liyou) > 500:
- return Response({
- 'code': 8,
- 'msg': '申诉理由不能超过500字',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证图片数量
- if len(shensu_tupian_urls) > 9:
- return Response({
- 'code': 9,
- 'msg': '最多只能上传9张图片',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 🔴🆕【关键】使用事务确保数据一致性
- from django.db import transaction
-
- with transaction.atomic():
- # 1. 查询处罚记录
- try:
- chufa_record = Chufajilu.objects.select_for_update().get(
- id=chufa_id,
- dashouid=yonghuid # 确保只能申诉自己的处罚记录
- )
- except Chufajilu.DoesNotExist:
- return Response({
- 'code': 10,
- 'msg': '处罚记录不存在或无权申诉',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 2. 验证处罚记录状态(必须是待处理状态0)
- if chufa_record.sqzhuangtai != 0:
- return Response({
- 'code': 11,
- 'msg': '该处罚记录已处理,无法申诉',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 验证订单是否存在(可选,根据需求决定)
- # 这里可以根据dingdan_id去查询订单表,但根据需求描述,我们已经有了处罚记录
- # 处罚记录中包含了dingdan_id,如果dingdan_id不存在,说明订单可能被删除
- # 但根据业务逻辑,有处罚记录就应该有订单,所以这里可以跳过这个检查
-
- # 4. 检查是否已有申诉记录(防止重复申诉)
- if chufa_record.ssliyou or chufa_record.sqzhuangtai == 3:
- # 检查是否已经有申诉图片
- existing_shensu_tupian = Chufatupian.objects.filter(
- dingdan_id=chufa_record.dingdan_id,
- yonghuid=yonghuid
- ).exists()
-
- if existing_shensu_tupian:
- return Response({
- 'code': 12,
- 'msg': '您已经提交过申诉,请等待处理结果',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 更新处罚记录
- chufa_record.ssliyou = shensu_liyou
- chufa_record.sqzhuangtai = 3 # 状态改为申诉中
- chufa_record.save()
-
- # 6. 保存申诉图片(如果有)
- saved_tupian_urls = []
- if shensu_tupian_urls:
- for tupian_url in shensu_tupian_urls:
- if tupian_url: # 确保URL不为空
- chufa_tupian = Chufatupian.objects.create(
- dingdan_id=chufa_record.dingdan_id,
- yonghuid=yonghuid,
- tupian=tupian_url
- )
- saved_tupian_urls.append(tupian_url)
-
- # 7. 构建返回数据
- response_data = {
- 'chufa_id': chufa_record.id,
- 'dingdan_id': chufa_record.dingdan_id or '',
- 'shensu_liyou': shensu_liyou,
- 'shensu_tupian_urls': saved_tupian_urls,
- 'sqzhuangtai': chufa_record.sqzhuangtai,
- 'update_time': chufa_record.update_time.strftime(
- '%Y-%m-%d %H:%M:%S') if chufa_record.update_time else ''
- }
-
- # 记录操作日志
- logger.info(f"打手申诉成功 - 用户ID: {yonghuid}, 处罚ID: {chufa_id}, 订单ID: {chufa_record.dingdan_id}")
-
- return Response({
- 'code': 0,
- 'msg': '申诉提交成功',
- 'data': response_data
- })
-
- except Exception as e:
- logger.error(f"打手申诉异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
- return Response({
- 'code': 99,
- 'msg': '系统繁忙,请稍后重试',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-
-
-class ShangjiaChufaJiluHuoquView(APIView):
- """
- 商家获取处罚记录接口
- 路径: /yonghu/sjcfjlhq
- 方法: POST
- 权限: JWT Token + 商家权限验证
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 获取当前用户信息
- user_main = request.user
- yonghuid = user_main.yonghuid
-
- # 通过反向关系获取商家扩展信息
- try:
- shangjia_profile = user_main.shop_profile
- # 再次确认账号状态
- if shangjia_profile.zhuangtai != 1:
- return Response({
- 'code': 3,
- 'msg': '商家账号状态异常,无法查看处罚记录',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
- except UserShangjia.DoesNotExist:
- return Response({
- 'code': 2,
- 'msg': '用户不是商家身份',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 判断请求类型
- qingqiu_tongji = request.data.get('qingqiu_tongji', False)
-
- if qingqiu_tongji:
- return self.get_tongji_info(yonghuid)
- else:
- return self.get_chufa_list(yonghuid, request.data)
-
- except Exception as e:
- logger.error(f"商家获取处罚记录异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
- return Response({
- 'code': 99,
- 'msg': '系统繁忙,请稍后重试',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def get_tongji_info(self, yonghuid):
- """
- 获取统计信息
- """
- try:
- # 🔴 查询条件:qingqiuid = 当前商家ID
- stats = Chufajilu.objects.filter(qingqiuid=yonghuid).aggregate(
- zongshu=Count('id'),
- daichuli=Count(
- Case(
- When(Q(sqzhuangtai=0) | Q(sqzhuangtai=3), then=1),
- output_field=IntegerField()
- )
- ),
- yichuli=Count(
- Case(
- When(Q(sqzhuangtai=1) | Q(sqzhuangtai=2), then=1),
- output_field=IntegerField()
- )
- )
- )
-
- tongji_data = {
- 'zongshu': stats.get('zongshu', 0) or 0,
- 'daichuli': stats.get('daichuli', 0) or 0,
- 'yichuli': stats.get('yichuli', 0) or 0
- }
-
- return Response({
- 'code': 0,
- 'msg': '获取统计信息成功',
- 'data': tongji_data
- })
-
- except Exception as e:
- logger.error(f"商家获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}")
- return Response({
- 'code': 1,
- 'msg': '获取统计信息失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def get_chufa_list(self, yonghuid, request_data):
- """
- 获取处罚记录列表(商家端)
- 🔴 只修改图片查询逻辑,字段保持原样
- """
- try:
- # 获取分页参数
- page = int(request_data.get('page', 1))
- page_size = int(request_data.get('page_size', 10))
-
- if page_size > 50:
- page_size = 50
- if page_size < 1:
- page_size = 10
-
- # 获取状态筛选参数
- zhuangtai = request_data.get('zhuangtai', 0)
-
- # 🔴 查询条件:qingqiuid = 当前商家ID
- queryset = Chufajilu.objects.filter(qingqiuid=yonghuid)
-
- if zhuangtai == 0:
- queryset = queryset.filter(Q(sqzhuangtai=0) | Q(sqzhuangtai=3))
- elif zhuangtai == 1:
- queryset = queryset.filter(Q(sqzhuangtai=1) | Q(sqzhuangtai=2))
- else:
- pass
-
- # 按创建时间倒序排列
- queryset = queryset.order_by('-create_time')
-
- # 分页逻辑
- paginator = Paginator(queryset, page_size)
-
- try:
- page_obj = paginator.page(page)
- except:
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': {
- 'list': [],
- 'has_more': False,
- 'page': page,
- 'page_size': page_size,
- 'current_count': 0
- }
- })
-
- chufa_records = list(page_obj)
-
- if not chufa_records:
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': {
- 'list': [],
- 'has_more': False,
- 'page': page,
- 'page_size': page_size,
- 'current_count': 0
- }
- })
-
- # 🔴【核心修复】收集所有需要查询图片的组合
- dingdan_ids = []
- dashou_ids = []
-
- for record in chufa_records:
- if record.dingdan_id:
- dingdan_ids.append(record.dingdan_id)
- if record.dashouid: # 打手ID
- dashou_ids.append(record.dashouid)
-
- # 去重
- dingdan_ids = list(set(dingdan_ids))
- dashou_ids = list(set(dashou_ids))
-
- # 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误)
- # 1. 查询证据图片(商家自己上传的)
- zhengju_mapping = {}
- if dingdan_ids:
- # 构建Q对象:dingdan_id在列表中 AND yonghuid=当前商家ID
- zhengju_q = Q(dingdan_id__in=dingdan_ids, yonghuid=yonghuid)
- zhengju_queryset = Chufatupian.objects.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian')
-
- for item in zhengju_queryset:
- key = f"{item['dingdan_id']}_{item['yonghuid']}"
- if key not in zhengju_mapping:
- zhengju_mapping[key] = []
- if item['tupian']:
- zhengju_mapping[key].append(item['tupian'])
-
- # 2. 查询申诉图片(打手上传的)
- shensu_mapping = {}
- if dingdan_ids and dashou_ids:
- # 构建Q对象:dingdan_id在列表中 AND yonghuid在打手ID列表中
- shensu_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=dashou_ids)
- shensu_queryset = Chufatupian.objects.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian')
-
- for item in shensu_queryset:
- key = f"{item['dingdan_id']}_{item['yonghuid']}"
- if key not in shensu_mapping:
- shensu_mapping[key] = []
- if item['tupian']:
- shensu_mapping[key].append(item['tupian'])
-
- # 🔴【核心修复】组装返回数据 - 字段保持原样!
- chufa_list = []
- for record in chufa_records:
- dingdan_id = record.dingdan_id
- dashou_id = record.dashouid
-
- # 获取证据图片(商家自己上传的)
- zhengju_tupian = []
- if dingdan_id:
- key = f"{dingdan_id}_{yonghuid}"
- zhengju_tupian = zhengju_mapping.get(key, [])
-
- # 获取申诉图片(打手上传的)
- shensu_tupian = []
- if dingdan_id and dashou_id:
- key = f"{dingdan_id}_{dashou_id}"
- shensu_tupian = shensu_mapping.get(key, [])
-
- # 🔴【字段保持原样!】与前端完全对应!
- chufa_data = {
- 'id': record.id,
- 'dingdan_id': record.dingdan_id or '',
- 'qingqiuid': record.dashouid or '', # 打手ID
- 'cfliyou': record.cfliyou or '',
- 'sqzhuangtai': record.sqzhuangtai or 0,
- 'ssliyou': record.ssliyou or '',
- 'jifen': record.jifen or 0,
- 'create_time': record.create_time.strftime('%Y-%m-%d %H:%M:%S') if record.create_time else '',
- 'update_time': record.update_time.strftime('%Y-%m-%d %H:%M:%S') if record.update_time else '',
- 'zhengju_tupian': zhengju_tupian, # 商家自己上传的证据图片
- 'shensu_tupian': shensu_tupian, # 打手上传的申诉图片
- }
- chufa_list.append(chufa_data)
-
- # 判断是否还有更多数据
- has_more = page_obj.has_next()
-
- response_data = {
- 'list': chufa_list,
- 'has_more': has_more,
- 'page': page,
- 'page_size': page_size,
- 'current_count': len(chufa_list)
- }
-
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': response_data
- })
-
- except ValueError as e:
- logger.error(f"商家端参数错误 - 用户ID: {yonghuid}, 错误: {str(e)}")
- return Response({
- 'code': 4,
- 'msg': '参数格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- logger.error(f"商家获取处罚记录列表异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
- return Response({
- 'code': 5,
- 'msg': '获取记录列表失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-from .models import UserMain, UserKefu
-
-logger = logging.getLogger(__name__)
-
-class KefuLoginView(APIView):
- """
- 客服登录接口(支持手机号重复)
- 请求:POST /yonghu/kefujinru
- 请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"}
- 返回:
- code=0 成功,data包含token,nicheng,yonghuid
- code=4 账号被封禁(提示“账号已被封禁”)
- 其他错误统一 code=1 msg="登录失败"
- """
- 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 or not erjimima:
- return Response({
- 'code': 1,
- 'msg': '登录失败', # 统一提示,不暴露具体原因
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 获取客户端真实IP
- client_ip = self.get_client_ip(request)
-
- # 3. 查询所有手机号匹配且类型为客服的用户
- # 注意:UserMain 中需要有 user_type 字段,且值为 'kefu' 代表客服
- user_mains = UserMain.objects.filter(
- phone=phone,
- user_type='kefu' # 只查找客服账号
- ).select_related('kefu_profile') # 预加载客服扩展表
-
- if not user_mains.exists():
- 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
-
- for user in user_mains:
- # 验证主密码(明文比较,按您的要求)
- if user.password != password:
- continue
-
- # 检查是否存在客服扩展表
- try:
- kefu = user.kefu_profile
- except UserKefu.DoesNotExist:
- continue
-
- # 验证二级密码(明文比较)
- if kefu.erjimima != 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)
- 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.last_login_time = timezone.now()
- target_user.save(update_fields=['ip', 'last_login_time'])
-
- # 6. 生成JWT token
- refresh = RefreshToken.for_user(target_user)
- token = str(refresh.access_token)
-
- # 7. 返回成功数据
- return Response({
- 'code': 0,
- 'msg': '登录成功',
- 'data': {
- 'token': token,
- 'nicheng': target_kefu.nicheng,
- 'yonghuid': target_user.yonghuid,
- }
- })
-
- 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')
-
-
-
-
-
-
-logger = logging.getLogger(__name__)
-
-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.yonghuid}")
- 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. 验证用户类型是否为客服
- if current_user.user_type != 'kefu':
- logger.warning(f"统计接口用户类型非客服: {current_user.yonghuid}")
- return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 5. 尝试获取客服扩展表数据
- try:
- kefu_profile = current_user.kefu_profile # OneToOneField 反向关系
- except UserKefu.DoesNotExist:
- logger.warning(f"统计接口客服扩展表不存在: {current_user.yonghuid}")
- 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)
-
-
-
-
-
-
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-import logging
-from django.core.exceptions import ObjectDoesNotExist
-
-from yonghu.models import UserMain, UserKefu
-from shangpin.models import ShangpinLeixing # 假设商品类型表名,请根据实际调整
-
-logger = logging.getLogger(__name__)
-
-class KefuGetOrderTypesView(APIView):
- """
- 客服获取订单类型列表接口(平台订单用)
- 请求:POST /yonghu/kfhqptddlx
- 参数:{"phone": "13800138000"}
- 认证:JWT
- 返回:code=0 + 类型列表
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- if not phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- current_user = request.user
-
- # 2. 验证手机号一致性
- if getattr(current_user, 'phone', '') != phone:
- logger.warning(f"手机号不匹配: 请求phone={phone}, 用户phone={current_user.phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 验证用户类型及客服状态
- if current_user.user_type != 'kefu':
- logger.warning(f"用户类型非客服: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- kefu_profile = current_user.kefu_profile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu_profile.zhuangtai != 1:
- logger.warning(f"客服账号已被禁用: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 4. 查询订单类型(商品类型)
- try:
- # 只查询需要的字段,减少数据传输
- types_qs = ShangpinLeixing.objects.all().only('id', 'jieshao', 'tupian_url')
- type_list = [
- {
- 'id': t.id,
- 'biaoti': t.jieshao or '', # 假设商品类型的标题字段为 jieshao
- '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': []})
-
-
-
-
-
-
-
-import time
-import logging
-from django.db.models import Q, Count
-from django.core.exceptions import ObjectDoesNotExist
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-
-from yonghu.models import UserMain, UserKefu, UserBoss
-from dingdan.models import Dingdan, DingdanPingtai
-
-logger = logging.getLogger(__name__)
-
-
-
-
-
-from rest_framework.views import APIView
-
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-from django.db.models import Q, Count
-from django.core.paginator import Paginator
-
-class KefuGetOrderListView(APIView):
- """
- 客服获取平台订单列表接口(发单平台=1,非跨平台)
- 需要权限:002ab(平台订单管理)
- 请求参数:
- username: 客服账号(用于权限校验)
- page: 页码
- pageSize: 每页条数
- dingdan_id: 订单ID(精确搜索)
- laoban_id: 老板ID(搜索平台扩展表中的老板)
- dashou_id: 打手ID(搜索主表中的接单打手)
- leixing: 商品类型ID
- zhuangtai: 状态(多个用逗号分隔,如 "1,7")
- date: 日期(YYYY-MM-DD),筛选订单创建时间
- nicheng: 游戏昵称(订单主表的 nicheng 字段)
- jieshao: 订单介绍(订单主表的 jieshao 字段)
- clkf: 处理客服(订单主表的 clkf 字段)
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- start_total = time.time()
-
- # 1. 获取前端参数
- username = request.data.get('phone', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- # 3. 检查平台订单管理权限(002ab)
- if '002ab' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
-
- # 4. 获取分页和筛选参数
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 20))
-
- dingdan_id = request.data.get('dingdan_id', '').strip()
- laoban_id = request.data.get('laoban_id', '').strip()
- dashou_id = request.data.get('dashou_id', '').strip()
- leixing = request.data.get('leixing')
- zhuangtai = request.data.get('zhuangtai', '')
- date = request.data.get('date', '').strip()
- # 新增筛选条件
- nicheng = request.data.get('nicheng', '').strip()
- jieshao = request.data.get('jieshao', '').strip()
- clkf = request.data.get('clkf', '').strip()
-
- # ---------- 查询条件 ----------
- # 基础条件:平台发单 + 非跨平台
- base_q = Q(fadan_pingtai=1) & Q(is_cross=0)
-
- if dingdan_id:
- q_conditions = base_q & Q(dingdan_id=dingdan_id)
- else:
- q_conditions = base_q
- if leixing is not None:
- try:
- leixing_int = int(leixing)
- q_conditions &= Q(leixing_id=leixing_int)
- except (ValueError, TypeError):
- pass
- if zhuangtai and zhuangtai != 'all':
- try:
- status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
- if status_list:
- q_conditions &= Q(zhuangtai__in=status_list)
- except (ValueError, TypeError):
- pass
- if dashou_id:
- q_conditions &= Q(jiedan_dashou_id=dashou_id)
- if laoban_id:
- q_conditions &= Q(pingtai_kuozhan__laoban_id=laoban_id)
- if date:
- q_conditions &= Q(create_time__date=date)
- # 新增筛选条件
- if nicheng:
- q_conditions &= Q(nicheng__icontains=nicheng)
- if jieshao:
- q_conditions &= Q(jieshao__icontains=jieshao)
- if clkf:
- q_conditions &= Q(clkf__icontains=clkf)
-
- # ---------- 统计(仅按发单平台,非跨平台) ----------
- stats = Dingdan.objects.filter(fadan_pingtai=1, is_cross=0).aggregate(
- total_orders=Count('id'),
- completed_orders=Count('id', filter=Q(zhuangtai=3)),
- refund_orders=Count('id', filter=Q(zhuangtai=5)),
- pending_orders=Count('id', filter=Q(zhuangtai__in=[4,8]))
- )
-
- # ---------- 状态计数(所有状态都返回,非跨平台) ----------
- status_counts = {
- 'all': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0).count(),
- '1,7': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai__in=[1,7]).count(),
- '2': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=2).count(),
- '8': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=8).count(),
- '3': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=3).count(),
- '4': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=4).count(),
- '5': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=5).count(),
- '6': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=6).count(),
- }
-
- # ---------- 分页查询 ----------
- total_count = Dingdan.objects.filter(q_conditions).count()
-
- orders_qs = Dingdan.objects.filter(q_conditions).select_related(
- 'pingtai_kuozhan'
- ).only(
- 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', 'fadan_pingtai',
- 'jine', 'tupian', 'create_time', 'nicheng', 'jiedan_dashou_id',
- 'pingtai_kuozhan__laoban_id'
- ).order_by('-create_time')[(page-1)*page_size : page*page_size]
-
- # ---------- 获取老板昵称 ----------
- laoban_ids = [o.pingtai_kuozhan.laoban_id for o in orders_qs if hasattr(o, 'pingtai_kuozhan') and o.pingtai_kuozhan.laoban_id]
- nickname_map = {}
- if laoban_ids:
- users = UserMain.objects.filter(yonghuid__in=laoban_ids).select_related('boss_profile').only(
- 'yonghuid', 'boss_profile__nickname'
- )
- for user in users:
- nickname_map[user.yonghuid] = user.boss_profile.nickname or '' if hasattr(user, 'boss_profile') else ''
-
- result_list = []
- for order in orders_qs:
- ext = getattr(order, 'pingtai_kuozhan', None)
- laoban_id = ext.laoban_id if ext else ''
- youxi_nicheng = nickname_map.get(laoban_id, order.nicheng or '')
- result_list.append({
- 'dingdan_id': order.dingdan_id or '',
- 'jieshao': order.jieshao or '',
- 'zhuangtai': order.zhuangtai,
- 'leixing': order.leixing_id,
- 'fadanpingtai': order.fadan_pingtai,
- 'jiage': float(order.jine) if order.jine else 0.00,
- 'tupian_url': order.tupian or '',
- 'fadanshijian': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '',
- 'youxi_nicheng': youxi_nicheng
- })
-
- response_data = {
- 'stats': {
- 'totalOrders': stats['total_orders'] or 0,
- 'completedOrders': stats['completed_orders'] or 0,
- 'refundOrders': stats['refund_orders'] or 0,
- 'pendingOrders': stats['pending_orders'] or 0
- },
- 'statusCounts': status_counts,
- 'list': result_list,
- 'total': total_count,
- 'page': page,
- 'pageSize': page_size
- }
-
- logger.info(f"平台订单接口总耗时: {time.time()-start_total:.3f}s")
- return Response({'code': 0, 'data': response_data})
-
-class KefuGetShangjiaOrderListView(APIView):
- """
- 客服获取商家订单列表接口(发单平台=2,非跨平台)
- 需要权限:002ac(商家订单管理)—— 如不区分可使用 002ab
- 请求参数:
- username: 客服账号(用于权限校验)
- page: 页码
- pageSize: 每页条数
- dingdan_id: 订单ID(精确搜索)
- shangjia_id: 商家ID(搜索商家扩展表中的商家ID)
- dashou_id: 打手ID(搜索主表中的接单打手)
- leixing: 商品类型ID
- zhuangtai: 状态(多个用逗号分隔,如 "1,7")
- date: 日期(YYYY-MM-DD),筛选订单创建时间
- nicheng: 游戏昵称(订单主表的 nicheng 字段)
- jieshao: 订单介绍(订单主表的 jieshao 字段)
- clkf: 处理客服(订单主表的 clkf 字段)
- shangjia_nicheng: 商家昵称(商家扩展表中的昵称)
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- start_total = time.time()
-
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- # 3. 检查商家订单管理权限(使用 002ac,如果没有可改用 002ab)
- if '002ac' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看商家订单列表'})
-
- # 4. 获取分页和筛选参数
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 20))
-
- dingdan_id = request.data.get('dingdan_id', '').strip()
- shangjia_id = request.data.get('shangjia_id', '').strip()
- dashou_id = request.data.get('dashou_id', '').strip()
- leixing = request.data.get('leixing')
- zhuangtai = request.data.get('zhuangtai', '')
- date = request.data.get('date', '').strip()
- # 新增筛选条件
- nicheng = request.data.get('nicheng', '').strip()
- jieshao = request.data.get('jieshao', '').strip()
- clkf = request.data.get('clkf', '').strip()
- shangjia_nicheng = request.data.get('shangjia_nicheng', '').strip()
-
- # ---------- 查询条件 ----------
- # 基础条件:商家发单 + 非跨平台
- base_q = Q(fadan_pingtai=2) & Q(is_cross=0)
-
- if dingdan_id:
- q_conditions = base_q & Q(dingdan_id=dingdan_id)
- else:
- q_conditions = base_q
- if leixing is not None:
- try:
- leixing_int = int(leixing)
- q_conditions &= Q(leixing_id=leixing_int)
- except (ValueError, TypeError):
- pass
- if zhuangtai and zhuangtai != 'all':
- try:
- status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
- if status_list:
- q_conditions &= Q(zhuangtai__in=status_list)
- except (ValueError, TypeError):
- pass
- if dashou_id:
- q_conditions &= Q(jiedan_dashou_id=dashou_id)
- if shangjia_id:
- q_conditions &= Q(shangjia_kuozhan__shangjia_id=shangjia_id)
- if date:
- q_conditions &= Q(create_time__date=date)
- if nicheng:
- q_conditions &= Q(nicheng__icontains=nicheng)
- if jieshao:
- q_conditions &= Q(jieshao__icontains=jieshao)
- if clkf:
- q_conditions &= Q(clkf__icontains=clkf)
- if shangjia_nicheng:
- # 通过商家扩展表关联 UserShangjia 的昵称
- q_conditions &= Q(shangjia_kuozhan__shangjia_id__in=UserMain.objects.filter(
- shop_profile__nicheng__icontains=shangjia_nicheng
- ).values_list('yonghuid', flat=True))
-
- # ---------- 统计(仅按发单平台,非跨平台) ----------
- stats = Dingdan.objects.filter(fadan_pingtai=2, is_cross=0).aggregate(
- total_orders=Count('id'),
- completed_orders=Count('id', filter=Q(zhuangtai=3)),
- refund_orders=Count('id', filter=Q(zhuangtai=5)),
- pending_orders=Count('id', filter=Q(zhuangtai__in=[4,8]))
- )
-
- # ---------- 状态计数(所有状态都返回,非跨平台) ----------
- status_counts = {
- 'all': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0).count(),
- '1,7': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai__in=[1,7]).count(),
- '2': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=2).count(),
- '8': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=8).count(),
- '3': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=3).count(),
- '4': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=4).count(),
- '5': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=5).count(),
- '6': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=6).count(),
- }
-
- # ---------- 分页查询 ----------
- total_count = Dingdan.objects.filter(q_conditions).count()
-
- orders_qs = Dingdan.objects.filter(q_conditions).select_related(
- 'shangjia_kuozhan'
- ).only(
- 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', 'fadan_pingtai',
- 'jine', 'tupian', 'create_time', 'nicheng', 'jiedan_dashou_id',
- 'shangjia_kuozhan__shangjia_id'
- ).order_by('-create_time')[(page-1)*page_size : page*page_size]
-
- # ---------- 获取商家昵称 ----------
- shangjia_ids = [o.shangjia_kuozhan.shangjia_id for o in orders_qs if hasattr(o, 'shangjia_kuozhan') and o.shangjia_kuozhan.shangjia_id]
- nickname_map = {}
- if shangjia_ids:
- users = UserMain.objects.filter(yonghuid__in=shangjia_ids).select_related('shop_profile').only(
- 'yonghuid', 'shop_profile__nicheng'
- )
- for user in users:
- nickname_map[user.yonghuid] = user.shop_profile.nicheng or '' if hasattr(user, 'shop_profile') else ''
-
- result_list = []
- for order in orders_qs:
- ext = getattr(order, 'shangjia_kuozhan', None)
- shangjia_id = ext.shangjia_id if ext else ''
- youxi_nicheng = order.nicheng or '' # 商家订单的玩家昵称
- result_list.append({
- 'dingdan_id': order.dingdan_id or '',
- 'jieshao': order.jieshao or '',
- 'zhuangtai': order.zhuangtai,
- 'leixing': order.leixing_id,
- 'fadanpingtai': order.fadan_pingtai,
- 'jiage': float(order.jine) if order.jine else 0.00,
- 'tupian_url': order.tupian or '',
- 'fadanshijian': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '',
- 'youxi_nicheng': youxi_nicheng
- })
-
- response_data = {
- 'stats': {
- 'totalOrders': stats['total_orders'] or 0,
- 'completedOrders': stats['completed_orders'] or 0,
- 'refundOrders': stats['refund_orders'] or 0,
- 'pendingOrders': stats['pending_orders'] or 0
- },
- 'statusCounts': status_counts,
- 'list': result_list,
- 'total': total_count,
- 'page': page,
- 'pageSize': page_size
- }
-
- logger.info(f"商家订单接口总耗时: {time.time()-start_total:.3f}s")
- return Response({'code': 0, 'data': response_data})
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-import logging
-from django.db import transaction
-from django.core.exceptions import ObjectDoesNotExist
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-
-from yonghu.models import UserMain, UserKefu, UserDashou
-from dingdan.models import Dingdan
-from shangpin.models import Huiyuangoumai # 假设会员购买模型存在
-
-logger = logging.getLogger(__name__)
-
-class KefuChangeDashouView(APIView):
- """
- 客服更换打手接口
- 请求:POST /yonghu/kefughds
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "new_dashou_id": "新打手ID"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- new_dashou_id = request.data.get('new_dashou_id', '').strip()
-
- if not phone or not dingdan_id or not new_dashou_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.user_type != 'kefu':
- logger.warning(f"用户类型非客服: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- kefu = current_user.kefu_profile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单
- try:
- order = Dingdan.objects.get(dingdan_id=dingdan_id)
- except Dingdan.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 验证订单状态是否允许更换打手(进行中2 或 结算中8)
- if order.zhuangtai not in [2, 8]:
- return Response({'code': 400, 'msg': '订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 查询新打手
- try:
- new_dashou_user = UserMain.objects.get(yonghuid=new_dashou_id)
- except UserMain.DoesNotExist:
- return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 6. 验证打手扩展表
- try:
- new_dashou_profile = new_dashou_user.dashou_profile
- except ObjectDoesNotExist:
- return Response({'code': 404, 'msg': '打手信息不完整'}, status=status.HTTP_404_NOT_FOUND)
-
- # 7. 验证打手状态(打手状态和账号状态必须为1)
- if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1:
- return Response({'code': 400, 'msg': '打手状态不允许接单'}, status=status.HTTP_400_BAD_REQUEST)
- # 8. 验证打手积分是否足够(至少5分)
- if new_dashou_profile.jifen < 5:
- return Response({'code': 400, 'msg': '打手积分不足,无法接单'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 8. 根据订单的抢单要求类型进行额外验证
- yaoqiuleixing = order.yaoqiuleixing
-
- if yaoqiuleixing == 1: # 会员抢单
- huiyuan_id = order.huiyuan_id
- if not huiyuan_id:
- return Response({'code': 400, 'msg': '订单会员ID为空'}, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- # 查询打手的会员购买记录
- huiyuan_goumai = Huiyuangoumai.objects.get(
- huiyuan_id=huiyuan_id,
- yonghu_id=new_dashou_id # 假设 yonghu_id 字段存储用户ID
- )
- except Huiyuangoumai.DoesNotExist:
- return Response({'code': 400, 'msg': '打手未购买该订单要求的会员'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 检查会员是否过期(调用模型方法)
- is_expired = huiyuan_goumai.jiance_shifou_daoqi()
- if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1:
- return Response({'code': 400, 'msg': '打手的会员已过期'}, status=status.HTTP_400_BAD_REQUEST)
-
- elif yaoqiuleixing == 2: # 押金抢单
- order_yajin = order.yongjin # 假设订单的 yongjin 字段存储押金要求
- if order_yajin is None:
- return Response({'code': 400, 'msg': '订单押金为空'}, status=status.HTTP_400_BAD_REQUEST)
-
- if new_dashou_profile.yajin < order_yajin:
- return Response({'code': 400, 'msg': '打手押金不足'}, status=status.HTTP_400_BAD_REQUEST)
- else:
- return Response({'code': 400, 'msg': '订单抢单要求类型错误'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 9. 所有验证通过,开始更换打手(使用事务)
- with transaction.atomic():
- # 9.1 更新新打手扩展表
- new_dashou_profile.zhuangtai = 0 # 设为接单中
- new_dashou_profile.jiedanzongliang += 1
- new_dashou_profile.jinrijiedan += 1
- new_dashou_profile.jinyuejiedan += 1
- new_dashou_profile.save()
-
- # 9.2 处理原打手(如果存在)
- old_dashou_id = order.jiedan_dashou_id
- if old_dashou_id:
- try:
- old_dashou_user = UserMain.objects.get(yonghuid=old_dashou_id)
- old_dashou_profile = old_dashou_user.dashou_profile
- old_dashou_profile.zhuangtai = 1 # 恢复空闲
- old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1(业务逻辑)
- old_dashou_profile.save()
- logger.info(f"原打手 {old_dashou_id} 状态已更新")
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"原打手 {old_dashou_id} 不存在或信息不完整,跳过更新")
- except Exception as e:
- logger.error(f"更新原打手时异常: {str(e)}", exc_info=True)
-
- # 9.3 更新订单
- order.jiedan_dashou_id = new_dashou_id
- order.zhuangtai = 2 # 进行中
- order.clkf = current_user.yonghuid # 记录处理客服ID
- order.save()
-
- return Response({'code': 0, 'msg': '更换打手成功'})
-
-
-
-
-
-class KefuRecoverOrderView(APIView):
- """
- 客服恢复订单接口(仅状态9可恢复)
- 请求:POST /yonghu/kefuhf
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.user_type != 'kefu':
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- kefu = current_user.kefu_profile
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单
- try:
- order = Dingdan.objects.get(dingdan_id=dingdan_id)
- except Dingdan.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 验证订单状态是否为9(未付款)
- if order.zhuangtai != 9:
- return Response({'code': 400, 'msg': '订单状态不允许恢复'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 根据是否有指定ID决定新状态
- if order.zhiding_id:
- new_status = 7 # 指定中
- else:
- new_status = 1 # 下单中
-
- # 6. 更新订单
- order.zhuangtai = new_status
- order.clkf = current_user.yonghuid
- order.save()
-
- return Response({'code': 0, 'msg': '恢复成功', 'data': {'new_status': new_status}})
-
-
-
-
-
-
-
-
-# dingdan/views.py
-import logging
-import random
-import hashlib
-import requests
-import xmltodict
-import time
-from decimal import Decimal
-from django.db import transaction
-from django.conf import settings
-from django.core.exceptions import ObjectDoesNotExist
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-
-from yonghu.models import UserMain, UserKefu, UserDashou
-from dingdan.models import Dingdan, DingdanPingtai, Tuikuanjilu # 注意:Tuikuanjilu 模型未在本次提供,但假设存在
-
-logger = logging.getLogger(__name__)
-
-
-class KefuPlatformRefundView(APIView):
- """
- 客服平台订单退款接口(调用微信支付)
- 请求:POST /yonghu/kefupttk
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "tuikuan_liyou": "退款理由(可选)"
- }
- 认证:JWT
- 返回:code=0 成功,code=其他 失败
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.user_type != 'kefu':
- logger.warning(f"用户类型非客服: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- kefu = current_user.kefu_profile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单并加锁
- try:
- with transaction.atomic():
- order = Dingdan.objects.select_related('pingtai_kuozhan').select_for_update().get(dingdan_id=dingdan_id)
-
- # 4. 验证平台订单
- if order.fadan_pingtai != 1:
- return Response({'code': 400, 'msg': '该订单不是平台订单'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 验证状态允许退款(根据模型,状态值:1下单中,2进行中,3已完成,4退款审核,5已退款,6退款失败,7指定中,8结算中)
- allowed_statuses = [1, 2, 4, 7, 8] # 允许退款的已支付或处理中状态
- if order.zhuangtai not in allowed_statuses:
- return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 6. 获取微信交易号(如果存在)
- wechat_transaction_id = getattr(order, 'wechat_transaction_id', None)
-
- # 7. 调用微信退款接口
- refund_result = self.call_wechat_refund(
- dingdan_id=order.dingdan_id,
- jine=order.jine,
- transaction_id=wechat_transaction_id
- )
-
- if refund_result['code'] != 0:
- # 微信退款失败,直接返回错误,数据库不更新
- logger.error(f"微信退款失败,订单 {dingdan_id},原因:{refund_result['msg']}")
- return Response({'code': 400, 'msg': refund_result['msg']}, status=status.HTTP_400_BAD_REQUEST)
-
- # 8. 更新订单状态为5(已退款)
- order.zhuangtai = 5
- order.clkf = current_user.phone # 记录处理客服手机号
- if tuikuan_liyou:
- order.tkly = tuikuan_liyou
- order.save()
-
- # 9. 更新打手数据
- jiedan_dashou_id = order.jiedan_dashou_id
- if jiedan_dashou_id:
- try:
- dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
- dashou_profile = dashou_user.dashou_profile
- dashou_profile.tuikuanliang += 1
- dashou_profile.zhuangtai = 1 # 打手状态设为空闲
- dashou_profile.save()
- logger.info(f"平台退款:打手 {jiedan_dashou_id} 退款订单+1")
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"平台退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
-
- # ========== 新增:商品和店铺每日统计 ==========
- try:
- # 商品每日统计(只要有商品ID就执行)
- if order.shangpin_id:
- # 从扩展表获取已计算好的收益值(下单时已存储)
- pingtai_shouyi = Decimal('0.00')
- dianpu_shouyi = Decimal('0.00')
- if hasattr(order, 'pingtai_kuozhan'):
- pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00')
- dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
-
- update_shangpin_daily_stat(
- shangpin_id=order.shangpin_id,
- caozuo_leixing=3, # 3 = 退款
- dingdan_jiage=order.jine,
- pingtai_shouyi=pingtai_shouyi,
- dianpu_zongshouyi=dianpu_shouyi
- )
- logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
-
- # 店铺每日统计(仅当存在店铺ID时执行)
- dianpu_id = None
- if hasattr(order, 'pingtai_kuozhan'):
- dianpu_id = order.pingtai_kuozhan.dianpu_id
- if dianpu_id:
- dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
- update_dianpu_daily_stat(
- dianpu_id=dianpu_id,
- caozuo_leixing=3, # 3 = 退款
- dingdan_jiage=order.jine,
- dianpu_shouyi=dianpu_shouyi
- )
- logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
- except Exception as e:
- logger.error(f"商品/店铺统计更新失败: {e}")
- logger.error(traceback.format_exc())
- # 统计失败不影响主流程
- # =============================================
-
- # 10. 更新退款记录表(假设存在 Tuikuanjilu 模型,字段需根据实际调整)
-
- #更新支出记录
- from dingdan.utils import update_daily_payout
- update_daily_payout(order.jine)
-
-
-
-
- try:
- from dingdan.models import Tuikuanjilu # 确保模型已导入
- refund_record, created = Tuikuanjilu.objects.get_or_create(
- dingdan_id=dingdan_id,
- defaults={
- 'dashouid': jiedan_dashou_id or '',
- 'qingqiuid': '',
- 'chuliid': phone,
- 'liyou': tuikuan_liyou or '客服退款',
- 'sqzhuangtai': 1, # 成功
- 'jine': order.jine,
- 'dashou_fencheng': order.dashou_fencheng or 0,
- 'jieshao': order.jieshao or '',
- 'beizhu': '客服平台退款',
- 'nicheng': order.nicheng or '',
- }
- )
- if not created:
- refund_record.sqzhuangtai = 1
- refund_record.chuliid = phone
- if tuikuan_liyou:
- refund_record.liyou = tuikuan_liyou
- refund_record.save()
- except Exception as e:
- logger.error(f"平台退款更新退款记录异常: {str(e)}", exc_info=True)
-
- # 11. 更新客服统计数据
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.jiedan_dashou_id,
- amount=order.dashou_fencheng,
- action=3 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
- logger.info(f"平台退款成功,订单 {dingdan_id},退款单号 {refund_result.get('refund_id')}")
- return Response({'code': 0, 'msg': '退款成功'})
-
- except Dingdan.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"平台退款接口异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def call_wechat_refund(self, dingdan_id, jine, transaction_id=None):
- """
- 调用微信支付退款接口
- :param dingdan_id: 商户订单号(我们的订单ID)
- :param jine: 退款金额
- :param transaction_id: 微信支付订单号(优先使用)
- :return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'}
- """
- # 获取配置
- appid = getattr(settings, 'WEIXIN_APPID', '')
- mch_id = getattr(settings, 'WEIXIN_MCHID', '')
- key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '')
- cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '')
- key_path = getattr(settings, 'WEIXIN_KEY_PATH', '')
-
- if not all([appid, mch_id, key, cert_path, key_path]):
- missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v]
- logger.error(f"微信支付配置缺失: {missing}")
- return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'}
-
- # 生成退款单号
- out_refund_no = self.generate_refund_no(dingdan_id)
- total_fee = int(float(jine) * 100)
- refund_fee = total_fee
- refund_desc = '客服退款'
-
- # 构造请求参数
- params = {
- 'appid': appid,
- 'mch_id': mch_id,
- 'nonce_str': self.generate_nonce_str(),
- 'out_refund_no': out_refund_no,
- 'total_fee': total_fee,
- 'refund_fee': refund_fee,
- 'refund_desc': refund_desc,
- }
- if transaction_id:
- params['transaction_id'] = transaction_id
- else:
- params['out_trade_no'] = dingdan_id # 使用订单ID作为商户订单号
-
- # 生成签名
- sorted_keys = sorted(params.keys())
- stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys])
- stringSignTemp = f"{stringA}&key={key}"
- sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
- params['sign'] = sign
-
- xml_data = self.dict_to_xml(params)
- url = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
-
- try:
- response = requests.post(
- url,
- data=xml_data,
- cert=(cert_path, key_path),
- timeout=10
- )
- result = xmltodict.parse(response.content)['xml']
- logger.info(f"微信退款响应: {result}")
- except Exception as e:
- logger.error(f"微信退款请求异常: {str(e)}", exc_info=True)
- return {'code': 500, 'msg': '微信退款请求异常'}
-
- if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
- return {'code': 0, 'msg': 'success', 'refund_id': out_refund_no}
- else:
- err_msg = result.get('err_code_des', result.get('return_msg', '未知错误'))
- logger.warning(f"微信退款失败: {err_msg}")
- return {'code': 400, 'msg': err_msg}
-
- def generate_nonce_str(self):
- import string
- return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
-
- def generate_refund_no(self, dingdan_id):
- timestamp = str(int(time.time()))
- rand = ''.join(random.choices('0123456789', k=4))
- return f"{dingdan_id}REF{timestamp}{rand}"
-
- def dict_to_xml(self, data):
- xml = ['']
- for k, v in data.items():
- xml.append(f'<{k}>{v}{k}>')
- xml.append('')
- return ''.join(xml)
-
-
-
-
-
-# dingdan/views.py
-import logging
-from django.db import transaction
-from django.core.exceptions import ObjectDoesNotExist
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-
-from yonghu.models import UserMain, UserKefu, UserDashou, UserShangjia
-from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu
-
-logger = logging.getLogger(__name__)
-
-
-class KefuRejectRefundView(APIView):
- """
- 客服拒绝退款接口
- 请求:POST /yonghu/kefujjtk
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "jujue_liyou": "拒绝理由(可选)"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- jujue_liyou = request.data.get('jujue_liyou', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.user_type != 'kefu':
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- kefu = current_user.kefu_profile
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单并加锁
- try:
- with transaction.atomic():
- order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
-
- # 4. 验证订单状态为退款审核中(4)
- if order.zhuangtai != 4:
- return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取必要字段
- jiedan_dashou_id = order.jiedan_dashou_id
- dashou_fencheng = order.dashou_fencheng
- fadan_pingtai = order.fadan_pingtai
-
- # 5. 验证打手分成金额是否存在
- if dashou_fencheng is None:
- return Response({'code': 400, 'msg': '订单缺少打手分成金额'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 6. 更新订单状态为退款失败(6)
- order.zhuangtai = 6
- order.clkf = current_user.phone # 记录处理客服手机号
- if jujue_liyou:
- order.tkly = jujue_liyou
- order.save()
-
- # 7. 给打手结算(返还分成)
- if jiedan_dashou_id:
- try:
- dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
- dashou_profile = dashou_user.dashou_profile
- # 增加打手相关统计数据
- dashou_profile.chengjiaozongliang += 1
- dashou_profile.yue += dashou_fencheng
- dashou_profile.zonge += dashou_fencheng
- dashou_profile.jinrishouyi += dashou_fencheng
- dashou_profile.jinyueshouyi += dashou_fencheng
- dashou_profile.save()
- logger.info(f"拒绝退款:打手 {jiedan_dashou_id} 获得分成 {dashou_fencheng}")
- except (UserMain.DoesNotExist, AttributeError):
- logger.warning(f"拒绝退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算")
-
- # 8. 如果是商家发单,更新商家成交订单数量
- if fadan_pingtai == 2:
- try:
- # 获取商家订单扩展表
- shangjia_kuozhan = order.shangjia_kuozhan
- shangjia_id = shangjia_kuozhan.shangjia_id
- if shangjia_id:
- shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
- shangjia_profile = shangjia_user.shop_profile
- shangjia_profile.chengjiao += 1
- shangjia_profile.save()
- logger.info(f"拒绝退款:商家 {shangjia_id} 成交订单+1")
-
- # ✅ 新方法(行为驱动,更安全)
- from houtai.utils import update_shangjia_daily
- update_shangjia_daily(
- yonghuid=shangjia_id,
- amount=Decimal(str(order.jine)),
- action=2 # 2 = 结算
- )
- except (ObjectDoesNotExist, UserMain.DoesNotExist, AttributeError):
- logger.warning(f"拒绝退款:商家信息不存在,跳过商家更新")
-
- # 9. 更新退款记录表
- try:
- refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id)
- refund_record.sqzhuangtai = 2 # 2表示拒绝
- refund_record.chuliid = phone
- refund_record.bhliyou = jujue_liyou # 记录驳回理由(假设模型有此字段)
- refund_record.save()
- logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}")
- except Tuikuanjilu.DoesNotExist:
- # 如果退款记录不存在,可以创建一条记录(可选)
- Tuikuanjilu.objects.create(
- dingdan_id=dingdan_id,
- dashouid=jiedan_dashou_id or '',
- qingqiuid='',
- chuliid=phone,
- liyou=jujue_liyou or '客服拒绝退款',
- sqzhuangtai=2,
- jine=order.jine,
- dashou_fencheng=dashou_fencheng or 0,
- jieshao=order.jieshao or '',
- beizhu='客服拒绝退款',
- nicheng=order.nicheng or '',
- bhliyou=jujue_liyou or '', # 假设模型有 bhliyou 字段
- )
- except Exception as e:
- logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True)
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.jiedan_dashou_id,
- amount=dashou_fencheng,
- action=2 # 2 表示成交(结算)
- )
- logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
- # 10. 更新客服统计数据
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- return Response({'code': 0, 'msg': '拒绝退款成功'})
-
- except Dingdan.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-
-
-
-
-
-
-
-class KefuMerchantRefundView(APIView):
- """
- 客服商家订单退款接口(发单平台=2)
- 请求:POST /yonghu/kefusjtk
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "tuikuan_liyou": "退款理由(可选)"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 客服身份验证(同前,省略以节省篇幅,但实际必须完整)
- current_user = request.user
- # ... 验证
- if getattr(current_user, 'phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.user_type != 'kefu':
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- kefu = current_user.kefu_profile
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
-
- # 查询订单,预加载商家扩展表
- try:
- order = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id)
- except Dingdan.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 验证商家订单
- if order.fadan_pingtai != 2:
- return Response({'code': 400, 'msg': '该订单不是商家订单'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证状态允许退款
- allowed_statuses = [1, 7, 2, 8, 4]
- if order.zhuangtai not in allowed_statuses:
- return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取必要数据
- jiedan_dashou_id = order.jiedan_dashou_id
- jine = order.jine
- if jine is None:
- return Response({'code': 400, 'msg': '订单金额缺失'}, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- shangjia_kuozhan = order.shangjia_kuozhan
- shangjia_id = shangjia_kuozhan.shangjia_id
- except ObjectDoesNotExist:
- return Response({'code': 400, 'msg': '订单商家信息不存在'}, status=status.HTTP_400_BAD_REQUEST)
-
- with transaction.atomic():
- # 更新订单
- order.zhuangtai = 5
- order.clkf = current_user.phone
- if tuikuan_liyou:
- order.tkly = tuikuan_liyou
- order.save()
-
- # 更新打手
- if jiedan_dashou_id:
- try:
- dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
- dashou_profile = dashou_user.dashou_profile
- dashou_profile.tuikuanliang += 1
- dashou_profile.zhuangtai = 1
- dashou_profile.save()
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"商家退款:打手 {jiedan_dashou_id} 不存在,跳过更新")
-
- # 更新商家
- try:
- shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
- shangjia_profile = shangjia_user.shop_profile
- shangjia_profile.tuikuan += 1
- shangjia_profile.yue += jine
- shangjia_profile.save()
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"商家退款:商家 {shangjia_id} 不存在,跳过更新")
-
- # 更新退款记录表
- try:
- refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id)
- refund_record.sqzhuangtai = 1
- refund_record.chuliid = phone
- refund_record.save()
- except Tuikuanjilu.DoesNotExist:
- pass
-
- # 更新客服统计
-
- # ✅ 新方法(行为驱动,更安全)
- from houtai.utils import update_shangjia_daily
- update_shangjia_daily(
- yonghuid=shangjia_id,
- amount=Decimal(str(order.jine)),
- action=3 # 3 = 退款
- )
-
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.jiedan_dashou_id,
- amount=order.dashou_fencheng,
- action=3 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
-
-
- kefu = current_user.kefu_profile
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- return Response({'code': 0, 'msg': '退款成功'})
-
-
-
-
-
-logger = logging.getLogger(__name__)
-
-class KefuRejectSettlementView(APIView):
- """
- 客服拒绝结算接口(仅平台订单,状态2/8)
- 请求:POST /yonghu/kefujjjs
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "tkly": "拒绝结算理由(必填)"
- }
- 认证:JWT
- 返回:code=0 成功,其他失败
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- tkly = request.data.get('tkly', '').strip()
-
- if not phone or not dingdan_id or not tkly:
- return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.user_type != 'kefu':
- logger.warning(f"用户类型非客服: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- kefu = current_user.kefu_profile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单,预加载平台扩展表
- try:
- order = Dingdan.objects.select_related('pingtai_kuozhan').get(dingdan_id=dingdan_id)
- except Dingdan.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 校验订单条件:平台发单(1)且状态为2或8
- if order.fadan_pingtai != 1 or order.zhuangtai not in [2, 8]:
- return Response({'code': 400, 'msg': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算'}, status=status.HTTP_400_BAD_REQUEST)
-
- jiedan_dashou_id = order.jiedan_dashou_id
- if not jiedan_dashou_id:
- return Response({'code': 400, 'msg': '订单暂无接单打手'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 使用事务更新
- with transaction.atomic():
- # 5.1 更新订单主表
- order.zhuangtai = 5 # 已退款
- order.tkly = tkly # 拒绝理由
- order.clkf = phone # 记录客服手机号
- order.save()
-
- # 5.2 更新平台订单扩展表:标记为拒绝结算(假设字段 jjjs_biaoshi)
- # 注意:需要确保 DingdanPingtai 模型中已添加 jjjs_biaoshi 字段,IntegerField,默认0
- if hasattr(order, 'pingtai_kuozhan') and order.pingtai_kuozhan:
- pingtai_ext = order.pingtai_kuozhan
- pingtai_ext.jjjs_biaoshi = 1 # 1表示拒绝结算
- pingtai_ext.save()
- else:
- # 理论上应该存在,但若不存在则记录日志
- logger.warning(f"订单 {dingdan_id} 缺少平台扩展表,无法标记拒绝结算")
-
- # 5.3 更新打手扩展表
- try:
- dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
- dashou_profile = dashou_user.dashou_profile
- dashou_profile.zhuangtai = 1 # 打手状态设为空闲
- dashou_profile.tuikuanliang += 1 # 退款订单总量+1
- dashou_profile.save()
- logger.info(f"拒绝结算:打手 {jiedan_dashou_id} 退款订单+1,状态空闲")
- except (UserMain.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"拒绝结算:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
- except Exception as e:
- logger.error(f"拒绝结算更新打手异常: {str(e)}", exc_info=True)
-
- # 5.4 更新客服扩展表统计数据
-
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.jiedan_dashou_id,
- amount=order.dashou_fencheng,
- action=3 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
-
-
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- return Response({'code': 0, 'msg': '拒绝结算成功'})
-
-
-
-class KefuTransferHallView(APIView):
- """
- 客服恢复大厅(转移大厅)接口
- 请求:POST /yonghu/kefuzydt
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证(同前,略,但必须完整实现)
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- if current_user.user_type != 'kefu':
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- try:
- kefu = current_user.kefu_profile
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单
- try:
- order = Dingdan.objects.get(dingdan_id=dingdan_id)
- except Dingdan.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 校验订单状态(允许状态:2,8,4)
- if order.zhuangtai not in [2, 8, 4]:
- return Response({'code': 400, 'msg': '订单状态不允许转移大厅'}, status=status.HTTP_400_BAD_REQUEST)
-
-
- if order.jiedan_dashou_id:
- UserDashou.objects.filter(user__yonghuid=order.jiedan_dashou_id).update(zhuangtai=1)
- else:
- pass
-
- # 5. 根据是否有指定ID决定新状态
- if order.zhiding_id:
- new_status = 7 # 指定中
- else:
- new_status = 1 # 下单中
-
- # 6. 使用事务更新
- with transaction.atomic():
- # 清空接单打手ID,更新状态,记录客服
- order.jiedan_dashou_id = None
- order.zhuangtai = new_status
- order.clkf = phone
- order.save()
-
- return Response({'code': 0, 'msg': '转移大厅成功'})
-
-
-class KefuCancelDesignationView(APIView):
- """
- 客服取消指定接口(仅状态7)
- 请求:POST /yonghu/kefuqxzd
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证(同前,略,但必须完整实现)
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- if current_user.user_type != 'kefu':
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- try:
- kefu = current_user.kefu_profile
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单
- try:
- order = Dingdan.objects.get(dingdan_id=dingdan_id)
- except Dingdan.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 校验订单状态为7(指定中)
-
- if order.zhuangtai != 7:
- print(999)
- return Response({'code': 400, 'msg': '仅状态为指定中(7)的订单可取消指定'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 使用事务更新
- with transaction.atomic():
- # 状态改为1(下单中),清空指定ID,记录客服
- order.zhuangtai = 1
- order.zhiding_id = None
- order.clkf = phone
- order.save()
-
- return Response({'code': 0, 'msg': '取消指定成功'})
-
-
-
-
-
-
-import logging
-from django.db.models import Q
-from django.core.exceptions import ObjectDoesNotExist
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-
-from yonghu.models import UserMain, UserKefu, UserDashou
-
-logger = logging.getLogger(__name__)
-
-class KefuGetDashouListView(APIView):
- """
- 客服获取打手列表接口(仅打手类型)
- 请求:POST /yonghu/kefuhqdslb
- 参数:{
- "phone": "客服手机号",
- "keyword": "搜索关键词(可选,搜索用户ID或昵称)",
- "page": "页码(从1开始)",
- "page_size": "每页数量"
- }
- 认证:JWT
- 返回:{
- "code": 0,
- "data": {
- "list": [打手对象],
- "has_more": true/false
- }
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('zhanghao', '').strip()
- keyword = request.data.get('keyword', '').strip()
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 20))
- except ValueError:
- return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
-
- if not phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.user_type != 'kefu':
- logger.warning(f"用户类型非客服: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- kefu = current_user.kefu_profile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 构建查询条件(只查询打手)
- # 通过 UserDashou 扩展表关联 UserMain
- queryset = UserDashou.objects.select_related('user').all()
-
- if keyword:
- # 搜索用户ID或打手昵称
- queryset = queryset.filter(
- Q(user__yonghuid__icontains=keyword) |
- Q(nicheng__icontains=keyword)
- )
-
- # 4. 计算总数并分页
- total = queryset.count()
- offset = (page - 1) * page_size
- dashou_list = queryset.order_by('-create_time')[offset:offset + page_size]
-
- # 5. 构建返回数据(只取需要的字段)
- result = []
- for dashou in dashou_list:
- user = dashou.user
- result.append({
- 'yonghuid': user.yonghuid,
- 'avatar': user.avatar or '',
- 'zaixianzhuangtai': dashou.zaixianzhuangtai,
- 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
- 'nicheng': dashou.nicheng or '',
- 'zhuangtai': dashou.zhuangtai,
- })
-
- # 6. 判断是否有更多数据
- has_more = (offset + len(dashou_list)) < total
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': result,
- 'has_more': has_more
- }
- })
-
-
-
-
-
-
-
-class KefuGetDashouDetailView(APIView):
- """
- 客服获取打手详情接口
- 请求:POST /yonghu/kefuhqdsxq
- 参数:{
- "phone": "客服手机号",
- "uid": "打手ID (yonghuid)"
- }
- 认证:JWT
- 返回:code=0 + 打手信息 + 会员列表
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- uid = request.data.get('uid', '').strip()
-
- if not phone or not uid:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.user_type != 'kefu':
- logger.warning(f"用户类型非客服: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- kefu = current_user.kefu_profile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询打手主表和扩展表
- try:
- # 使用 select_related 预加载 dashou_profile,减少数据库查询
- user_main = UserMain.objects.select_related('dashou_profile').get(
- yonghuid=uid
- )
- except UserMain.DoesNotExist:
- return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 验证用户类型是否为打手(可选,但建议验证)
- # 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册)
- try:
- dashou = user_main.dashou_profile
- except ObjectDoesNotExist:
- # 用户存在但未注册打手身份,返回基本信息
- user_info = {
- 'yonghuid': user_main.yonghuid,
- 'avatar': user_main.avatar or '',
- 'zaixianzhuangtai': 0,
- 'zhanghaozhuangtai': 0,
- 'zhuangtai': 0,
- 'nicheng': '',
- 'chenghao': '',
- 'dianhua': '',
- 'wechat': '',
- 'phone': user_main.phone or '',
- 'create_time': user_main.create_time,
- 'ip': user_main.ip or '',
- 'jiedanzongliang': 0,
- 'chengjiaozongliang': 0,
- 'tuikuanliang': 0,
- 'jinrijiedan': 0,
- 'yue': 0.00,
- 'zonge': 0.00,
- 'jifen': 0,
- 'yajin': 0.00,
- 'jinrishouyi': 0.00,
- 'jinyueshouyi': 0.00,
- 'yaoqingren': '',
- 'jieshao': '',
- }
- member_list = []
- return Response({
- 'code': 0,
- 'data': {
- 'user_info': user_info,
- 'member_list': member_list
- }
- })
-
- # 4. 构建打手信息
- user_info = {
- 'yonghuid': user_main.yonghuid,
- 'avatar': user_main.avatar or '',
- 'zaixianzhuangtai': dashou.zaixianzhuangtai,
- 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
- 'zhuangtai': dashou.zhuangtai,
- 'nicheng': dashou.nicheng or '',
- 'chenghao': dashou.chenghao or '',
- 'dianhua': dashou.dianhua or '',
- 'wechat': dashou.wechat or '',
- 'phone': user_main.phone or '',
- 'create_time': user_main.create_time,
- 'ip': user_main.ip or '',
- 'jiedanzongliang': dashou.jiedanzongliang,
- 'chengjiaozongliang': dashou.chengjiaozongliang,
- 'tuikuanliang': dashou.tuikuanliang,
- 'jinrijiedan': dashou.jinrijiedan,
- 'yue': float(dashou.yue) if dashou.yue else 0.00,
- 'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
- 'jifen': dashou.jifen,
- 'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
- 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
- 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
- 'yaoqingren': dashou.yaoqingren or '',
- 'jieshao': dashou.jieshao or '',
- }
-
- # 5. 查询打手会员信息(仅未过期)
- member_records = Huiyuangoumai.objects.filter(
- yonghu_id=uid
- ).order_by('-create_time')
-
- member_list = []
- for record in member_records:
- # 调用模型方法检查是否过期
- if hasattr(record, 'jiance_shifou_daoqi'):
- is_expired = record.jiance_shifou_daoqi()
- else:
- # 如果方法不存在,简单判断
- is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
-
- if not is_expired:
- # 获取会员详情
- try:
- huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id)
- huiyuan_name = huiyuan.jieshao
- except Huiyuan.DoesNotExist:
- huiyuan_name = '未知会员'
-
- member_list.append({
- 'id': record.id,
- 'huiyuan_name': huiyuan_name,
- 'daoqi_time': record.daoqi_time,
- 'is_active': record.huiyuan_zhuangtai == 1,
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'user_info': user_info,
- 'member_list': member_list
- }
- })
-
-
-
-
-from yonghu.models import Xiugaijilu # 确保已导入
-
-
-
-
-from houtai.utils import verify_kefu_permission
-
-logger = logging.getLogger(__name__)
-
-class KefuUpdateDashouView(APIView):
- permission_classes = [IsAuthenticated]
- """
- 客服修改打手信息接口
- 支持操作:
- - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb)
- - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb)
- - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd)
- - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee)
- - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee)
- - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee)
- - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg)
- """
- def post(self, request):
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- dashou_id = request.data.get('dashou_id', '').strip()
- caozuo = request.data.get('caozuo', '').strip()
- value = request.data.get('value') # 金额、积分、天数等
- huiyuan_id = request.data.get('huiyuan_id', '').strip() # 会员ID(加/减会员时使用)
-
- if not username or not dashou_id or not caozuo:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 权限校验(统一方法)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 查询打手是否存在
- try:
- dashou_user = UserMain.objects.get(yonghuid=dashou_id, user_type='dashou')
- dashou_profile = dashou_user.dashou_profile
- except UserMain.DoesNotExist:
- return Response({'code': 404, 'msg': '打手不存在'})
- except UserDashou.DoesNotExist:
- return Response({'code': 404, 'msg': '打手扩展信息缺失'})
-
- # 4. 根据操作类型执行(事务内)
- with transaction.atomic():
- try:
- # 记录修改前的值(用于日志)
- before = {
- 'yue': dashou_profile.yue,
- 'yajin': dashou_profile.yajin,
- 'jifen': dashou_profile.jifen,
- 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'zhuangtai': dashou_profile.zhuangtai,
- }
-
- # ---------- 余额操作 ----------
- if caozuo == 'jian_yue':
- if '001aa' not in permissions:
- return Response({'code': 403, 'msg': '无权限减打手余额'})
- amount = Decimal(str(value))
- if dashou_profile.yue < amount:
- return Response({'code': 400, 'msg': '余额不足'})
- dashou_profile.yue -= amount
- dashou_profile.save()
- after_yue = dashou_profile.yue
- # 记录日志
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2, # 2=打手
- xiugaitijiao=after_yue,
- xiugaitijiaoq=before['yue'],
- )
- return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after_yue)}})
-
- elif caozuo == 'jia_yue':
- if '001bb' not in permissions:
- return Response({'code': 403, 'msg': '无权限加打手余额'})
- amount = Decimal(str(value))
- dashou_profile.yue += amount
- dashou_profile.save()
- after_yue = dashou_profile.yue
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- xiugaitijiao=after_yue,
- xiugaitijiaoq=before['yue'],
- )
- return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after_yue)}})
-
- # ---------- 押金操作 ----------
- elif caozuo == 'jian_yajin':
- if '001aa' not in permissions:
- return Response({'code': 403, 'msg': '无权限减打手押金'})
- amount = Decimal(str(value))
- if dashou_profile.yajin < amount:
- return Response({'code': 400, 'msg': '押金不足'})
- dashou_profile.yajin -= amount
- dashou_profile.save()
- after_yajin = dashou_profile.yajin
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- yajin=after_yajin,
- yyajin=before['yajin'],
- )
- return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after_yajin)}})
-
- elif caozuo == 'jia_yajin':
- if '001bb' not in permissions:
- return Response({'code': 403, 'msg': '无权限加打手押金'})
- amount = Decimal(str(value))
- dashou_profile.yajin += amount
- dashou_profile.save()
- after_yajin = dashou_profile.yajin
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- yajin=after_yajin,
- yyajin=before['yajin'],
- )
- return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after_yajin)}})
-
- # ---------- 积分操作 ----------
- elif caozuo == 'jian_jifen':
- if '001cc' not in permissions:
- return Response({'code': 403, 'msg': '无权限减打手积分'})
- jifen = int(value)
- if dashou_profile.jifen < jifen:
- return Response({'code': 400, 'msg': '积分不足'})
- dashou_profile.jifen -= jifen
- dashou_profile.save()
- after_jifen = dashou_profile.jifen
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- jifen=after_jifen,
- yjifen=before['jifen'],
- )
- return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after_jifen}})
-
- elif caozuo == 'jia_jifen':
- if '001dd' not in permissions:
- return Response({'code': 403, 'msg': '无权限加打手积分'})
- jifen = int(value)
- dashou_profile.jifen += jifen
- dashou_profile.save()
- after_jifen = dashou_profile.jifen
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- jifen=after_jifen,
- yjifen=before['jifen'],
- )
- return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after_jifen}})
-
- # ---------- 状态修改 ----------
- elif caozuo == 'gai_zhanghaozhuangtai':
- if '001ee' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改打手账号状态'})
- new_status = int(value)
- if new_status not in [0, 1]:
- return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'})
- dashou_profile.zhanghaozhuangtai = new_status
- dashou_profile.save()
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- qitashuoming=f'修改账号状态为{new_status}',
- )
- return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}})
-
- elif caozuo == 'gai_zaixianzhuangtai':
- if '001ee' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改打手在线状态'})
- new_status = int(value)
- if new_status not in [0, 1]:
- return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'})
- dashou_profile.zaixianzhuangtai = new_status
- dashou_profile.save()
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- qitashuoming=f'修改在线状态为{new_status}',
- )
- return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}})
-
- elif caozuo == 'gai_zhuangtai':
- if '001ee' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改打手接单状态'})
- new_status = int(value)
- if new_status not in [0, 1]:
- return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'})
- dashou_profile.zhuangtai = new_status
- dashou_profile.save()
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- qitashuoming=f'修改接单状态为{new_status}',
- )
- return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}})
-
- # ---------- 会员操作 ----------
- elif caozuo == 'jia_huiyuan':
- if '001ff' not in permissions:
- return Response({'code': 403, 'msg': '无权限给打手加会员'})
- if not huiyuan_id:
- return Response({'code': 400, 'msg': '缺少会员ID'})
- days = int(value) if value else 30 # 默认加30天
- try:
- huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id)
- except Huiyuan.DoesNotExist:
- return Response({'code': 404, 'msg': '会员类型不存在'})
- # 获取或创建购买记录
- record, created = Huiyuangoumai.objects.select_for_update().get_or_create(
- yonghu_id=dashou_id,
- huiyuan_id=huiyuan_id,
- defaults={
- 'huiyuan_zhuangtai': 1,
- 'jieshao': huiyuan.jieshao,
- 'daoqi_time': timezone.now() + timedelta(days=days),
- }
- )
- if not created:
- # 延长有效期
- new_daoqi = record.daoqi_time + timedelta(days=days)
- record.daoqi_time = new_daoqi
- record.huiyuan_zhuangtai = 1
- record.save()
- # 记录修改日志
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- huiyuants=days,
- huiyuan_id=huiyuan_id,
- qitashuoming=f'加会员 {huiyuan_id},增加{days}天',
- )
- return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
-
- elif caozuo == 'jian_huiyuan':
- if '001gg' not in permissions:
- return Response({'code': 403, 'msg': '无权限给打手减会员'})
- if not huiyuan_id:
- return Response({'code': 400, 'msg': '缺少会员ID'})
- try:
- record = Huiyuangoumai.objects.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id)
- except Huiyuangoumai.DoesNotExist:
- return Response({'code': 404, 'msg': '该打手未购买此会员'})
- # 删除会员记录(或设置为过期,根据业务要求)
- record.delete()
- Xiugaijilu.objects.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.phone,
- leixing=2,
- huiyuan_id=huiyuan_id,
- qitashuoming=f'移除会员 {huiyuan_id}',
- )
- return Response({'code': 0, 'msg': '减会员成功'})
-
- else:
- return Response({'code': 400, 'msg': '未知操作类型'})
-
- except Exception as e:
- logger.error(f"修改打手信息异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-
-
-
-
-class KefuPunishmentListView(APIView):
- """
- 客服获取处罚记录列表接口
- 请求:POST /yonghu/kefu_cfgl
- 参数:{
- "phone": "客服手机号",
- "status": [0,3] 或 [1,2], # 状态数组
- "dingdan_id": "订单ID(可选)",
- "dashouid": "打手ID(可选)",
- "page": 1,
- "page_size": 20
- }
- 认证:JWT
- 返回:{
- "code": 0,
- "data": {
- "list": [处罚记录],
- "total": 总数,
- "stats": {
- "zongshu": 总记录数,
- "daichuli": 待处理数,
- "yichuli": 已处理数
- }
- }
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- status_list = request.data.get('status')
- dingdan_id = request.data.get('dingdan_id', '').strip()
- dashouid = request.data.get('dashouid', '').strip()
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 20))
- except ValueError:
- return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
-
- if not phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- # 1. 获取前端参数
- username = request.data.get('phone', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- # 3. 检查平台订单管理权限(002ab)
- if '005aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
-
- # 3. 获取统计信息(不分页)
- stats = {
- 'zongshu': Chufajilu.objects.count(),
- 'daichuli': Chufajilu.objects.filter(sqzhuangtai__in=[0, 3]).count(),
- 'yichuli': Chufajilu.objects.filter(sqzhuangtai__in=[1, 2]).count(),
- }
-
- # 4. 构建查询条件
- queryset = Chufajilu.objects.all()
-
- if status_list and isinstance(status_list, list):
- queryset = queryset.filter(sqzhuangtai__in=status_list)
-
- if dingdan_id:
- queryset = queryset.filter(dingdan_id=dingdan_id)
-
- if dashouid:
- queryset = queryset.filter(dashouid=dashouid)
-
- # 5. 分页
- total = queryset.count()
- offset = (page - 1) * page_size
- records = queryset.order_by('-create_time')[offset:offset + page_size]
-
- # 6. 构建返回列表
- list_data = []
- for item in records:
- list_data.append({
- 'dingdan_id': item.dingdan_id,
- 'dashouid': item.dashouid,
- 'qingqiuid': item.qingqiuid,
- 'cfliyou': item.cfliyou,
- 'sqzhuangtai': item.sqzhuangtai,
- 'create_time': item.create_time,
- 'update_time': item.update_time,
- # 其他字段按需添加
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': list_data,
- 'total': total,
- 'stats': stats
- }
- })
-
-
-
-
-
-
-import sys
-import time
-import traceback
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-
-from yonghu.models import UserMain, UserKefu
-from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia, Dashoutupian, Tuikuanjilu
-
-
-class KefuGetOrderDetailView(APIView):
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 强制打印到控制台并立即刷新
- def p(msg):
- #print(f"[订单详情] {msg}")
- sys.stdout.flush()
-
- #p("========== 视图开始执行 ==========")
-
- # 1. 打印原始请求数据
- #p(f"request.data: {request.data}")
-
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- #p(f"phone={phone}, dingdan_id={dingdan_id}")
-
- if not phone or not dingdan_id:
- #p("参数不完整,返回401")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 获取当前用户
- current_user = request.user
-
-
- # 验证手机号
- if getattr(current_user, 'phone', '') != phone:
- #p("手机号不匹配")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 验证用户类型
- if current_user.user_type != 'kefu':
- #p("用户类型不是客服")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 客服扩展表
- try:
- kefu = current_user.kefu_profile
- #p(f"kefu_profile: {kefu}")
- if kefu.zhuangtai != 1:
- #p(f"客服状态异常: {kefu.zhuangtai}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- except Exception as e:
- #p(f"获取客服扩展表异常: {e}")
- return Response({'code': 500, 'msg': '服务器错误'}, status=500)
-
- # 3. 查询订单
- try:
- #p(f"开始查询订单: {dingdan_id}")
- dingdan_obj = Dingdan.objects.select_related(
- 'pingtai_kuozhan', 'shangjia_kuozhan'
- ).get(dingdan_id=dingdan_id)
- #p("订单查询成功")
- except Dingdan.DoesNotExist:
- #p("订单不存在")
- return Response({'code': 404, 'msg': '订单不存在'}, status=404)
- except Exception as e:
- #p(f"订单查询异常: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '服务器错误'}, status=500)
-
- # 4. 构建基础数据
- try:
- response_data = {
- 'dingdan_id': dingdan_obj.dingdan_id or '',
- 'zhuangtai': dingdan_obj.zhuangtai or 0,
- 'fadanpingtai': dingdan_obj.fadan_pingtai or 0,
- 'jiage': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00,
- 'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00,
- 'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '',
- 'dashou_liuyan': dingdan_obj.dashou_liuyan or '',
- 'zhiding_id': dingdan_obj.zhiding_id or '',
- 'shangpin_id': dingdan_obj.shangpin_id or '',
- 'leixing': dingdan_obj.leixing_id or 0,
- 'tupian_url': dingdan_obj.tupian or '',
- 'jieshao': dingdan_obj.jieshao or '',
- 'beizhu': dingdan_obj.beizhu or '',
- 'tkly': dingdan_obj.tkly or '',
- 'nicheng': dingdan_obj.nicheng or '',
- 'clkf': dingdan_obj.clkf or '',
- 'chuangjianshijian': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.create_time else '',
- 'genggaishijian': dingdan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.update_time else '',
- }
- #p("基础数据构建完成")
- except Exception as e:
- #p(f"构建基础数据异常: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '服务器错误'}, status=500)
-
- # 5. 扩展表数据
- try:
- fadan_pingtai = dingdan_obj.fadan_pingtai
- if fadan_pingtai == 1:
- if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan:
- pingtai = dingdan_obj.pingtai_kuozhan
- response_data['pingtai_kuozhan'] = {
- 'laoban_id': pingtai.laoban_id or '',
- 'laoban_pingjia': pingtai.laoban_pingjia or '',
- }
- else:
- response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''}
- elif fadan_pingtai == 2:
- if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan:
- shangjia = dingdan_obj.shangjia_kuozhan
- response_data['shangjia_kuozhan'] = {
- 'shangjia_id': shangjia.shangjia_id or '',
- 'sjnicheng': shangjia.sjnicheng or '',
- 'shangjia_pingjia': shangjia.shangjia_pingjia or '',
- 'cfliyou': shangjia.cfliyou or '',
- 'sqzhuangtai': shangjia.sqzhuangtai if shangjia.sqzhuangtai is not None else 0,
- 'bhliyou': shangjia.bhliyou or '',
- }
- else:
- response_data['shangjia_kuozhan'] = {
- 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '',
- 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': ''
- }
- #p("扩展数据构建成功")
- except Exception as e:
- #p(f"扩展数据异常: {traceback.format_exc()}")
- return Response({'code': 500, 'msg': '服务器错误'}, status=500)
-
- # 6. 打手图片
- try:
- dashou_images = Dashoutupian.objects.filter(dingdan_id=dingdan_id).values_list('tupian', flat=True)
- response_data['dashou_images'] = [img for img in dashou_images if img]
- #p(f"打手图片数量: {len(response_data['dashou_images'])}")
- except Exception as e:
- #p(f"打手图片异常: {traceback.format_exc()}")
- response_data['dashou_images'] = []
-
- # 7. 退款理由
- try:
- tuikuan_record = Tuikuanjilu.objects.filter(dingdan_id=dingdan_id).order_by('-create_time').first()
- response_data['tuikuan_liyou'] = tuikuan_record.liyou if tuikuan_record and tuikuan_record.liyou else ''
- #p("退款理由查询完成")
- except Exception as e:
- #p(f"退款理由异常: {traceback.format_exc()}")
- response_data['tuikuan_liyou'] = ''
-
- #p("视图正常结束,返回成功")
- return Response({'code': 0, 'data': response_data})
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-class KefuPunishmentDetailView(APIView):
- """
- 客服获取处罚详情接口
- 请求:POST /yonghu/kefu_cf_detail
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID"
- }
- 认证:JWT
- 返回:code=0 + 处罚记录详情(含图片)
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 客服身份验证
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.user_type != 'kefu':
- logger.warning(f"用户类型非客服: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- kefu = current_user.kefu_profile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 查询处罚记录(按创建时间倒序取最新一条)
- try:
- record = Chufajilu.objects.filter(dingdan_id=dingdan_id).order_by('-create_time').first()
- if not record:
- return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"查询处罚记录失败: {e}")
- return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 查询商家证据图片
- zhengju_images = []
- shensu_images = []
- try:
- # 商家证据图片:上传者为请求人 (qingqiuid)
- zhengju_qs = Chufatupian.objects.filter(dingdan_id=dingdan_id, yonghuid=record.qingqiuid)
- zhengju_images = [item.tupian for item in zhengju_qs if item.tupian]
-
- # 打手申诉图片:上传者为打手 (dashouid)
- shensu_qs = Chufatupian.objects.filter(dingdan_id=dingdan_id, yonghuid=record.dashouid)
- shensu_images = [item.tupian for item in shensu_qs if item.tupian]
- except Exception as e:
- logger.error(f"查询图片失败: {e}")
-
- # 构建返回数据
- data = {
- 'dingdan_id': record.dingdan_id,
- 'dashouid': record.dashouid,
- 'qingqiuid': record.qingqiuid,
- 'chuliid': record.chuliid,
- 'cfliyou': record.cfliyou,
- 'sqzhuangtai': record.sqzhuangtai,
- 'bhliyou': record.bhliyou,
- 'jifen': record.jifen,
- 'ssliyou': record.ssliyou or '',
- 'zhengju_tupian': zhengju_images,
- 'shensu_tupian': shensu_images,
- 'create_time': record.create_time,
- 'update_time': record.update_time,
- }
-
- return Response({'code': 0, 'data': data})
-
-
-
-from django.db import transaction
-from decimal import Decimal
-
-class KefuPunishmentActionView(APIView):
- """
- 客服同意/驳回处罚接口
- 请求:POST /yonghu/kefu_cf_action
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "action": 1(同意) / 2(驳回),
- "reject_reason": "驳回理由(驳回时必填)"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- action = request.data.get('action')
- reject_reason = request.data.get('reject_reason', '').strip()
-
- if not phone or not dingdan_id or action not in [1, 2]:
- return Response({'code': 401, 'msg': '参数错误'}, status=status.HTTP_400_BAD_REQUEST)
-
- if action == 2 and not reject_reason:
- return Response({'code': 400, 'msg': '驳回理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 客服身份验证(完整验证,此处略)
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- if current_user.user_type != 'kefu':
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- try:
- kefu = current_user.kefu_profile
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 查询待处理的处罚记录(状态0或3)
- try:
- record = Chufajilu.objects.filter(
- dingdan_id=dingdan_id,
- sqzhuangtai__in=[0, 3]
- ).order_by('-create_time').first()
- if not record:
- exists = Chufajilu.objects.filter(dingdan_id=dingdan_id).exists()
- if exists:
- return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST)
- else:
- return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"查询处罚记录失败: {e}")
- return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- with transaction.atomic():
- # 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回
- duplicate_records = Chufajilu.objects.filter(
- dingdan_id=dingdan_id,
- sqzhuangtai__in=[0, 3]
- ).exclude(id=record.id)
- for dup in duplicate_records:
- dup.sqzhuangtai = 2
- dup.chuliid = phone
- dup.bhliyou = "系统自动驳回:存在重复处罚记录"
- dup.save()
- logger.info(f"自动驳回重复处罚记录 ID={dup.id}")
-
- if action == 1: # 同意
- # 可选验证打手存在
- if record.dashouid:
- try:
- dashou = UserDashou.objects.get(user__yonghuid=record.dashouid)
- except UserDashou.DoesNotExist:
- return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST)
-
- record.sqzhuangtai = 1
- record.chuliid = phone
- record.save()
-
- # 更新商家订单扩展表
- try:
- dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
- if hasattr(dingdan, 'shangjia_kuozhan'):
- dingdan.shangjia_kuozhan.sqzhuangtai = 1
- dingdan.shangjia_kuozhan.save()
- except Exception as e:
- logger.warning(f"更新商家扩展表失败: {e}")
-
- elif action == 2: # 驳回
- # 返还积分
- if record.jifen > 0 and record.dashouid:
- try:
- dashou = UserDashou.objects.get(user__yonghuid=record.dashouid)
- if dashou.jifen <= 1000: # 安全限制
- dashou.jifen += record.jifen
- dashou.save()
- logger.info(f"驳回处罚,打手{record.dashouid}返还积分{record.jifen}")
- else:
- logger.warning(f"打手积分异常({dashou.jifen}),不再返还")
- except UserDashou.DoesNotExist:
- logger.warning(f"打手{record.dashouid}不存在,无法返还积分")
-
- record.sqzhuangtai = 2
- record.chuliid = phone
- record.bhliyou = reject_reason
- record.save()
-
- try:
- dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
- if hasattr(dingdan, 'shangjia_kuozhan'):
- dingdan.shangjia_kuozhan.sqzhuangtai = 2
- dingdan.shangjia_kuozhan.bhliyou = reject_reason
- dingdan.shangjia_kuozhan.save()
- except Exception as e:
- logger.warning(f"更新商家扩展表失败: {e}")
-
- return Response({'code': 0, 'msg': '处理成功'})
-
-
-
-
-
-
-
-
-
-
-
-from dingdan.utils import update_daily_dispatch_stat
-from dingdan.models import PreSettlement
-
-logger = logging.getLogger(__name__)
-from shangpin.utils import update_shangpin_daily_stat
-from shangdian.utils import update_dianpu_daily_stat # 注意:您之前写的店铺统计方法名是 update_dianpu_daily_stat
-
-logger = logging.getLogger(__name__)
-from shangpin.utils import update_shangpin_daily_stat
-from shangdian.utils import update_dianpu_daily_stat # 注意:您之前写的店铺统计方法名是 update_dianpu_daily_stat
-
-
-
-class KefuForceCompleteView(APIView):
- """
- 客服强制结单接口(仅本地订单)
- 请求:POST /yonghu/kefuqiangzhijiedan
- 参数:{"phone": "客服手机号", "dingdan_id": "订单ID"}
- 认证:JWT,仅限状态正常的客服
- 逻辑:
- 1. 验证客服身份及状态
- 2. 查询订单并加锁(事务内)
- 3. 校验订单状态是否为8(结算中)
- 4. 校验订单必须有接单打手且打手分成金额有效
- 5. 给打手增加余额、成交统计、今日/本月收益
- 6. 如果是商家发单,更新商家成交订单数量
- 7. 更新订单状态为3(已完成)
- 8. 更新客服处理统计
- 9. 返回成功
- """
- permission_classes = [permissions.IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '参数缺失'})
-
- # 1. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'})
-
- if current_user.user_type != 'kefu':
- return Response({'code': 401, 'msg': '认证失败'})
-
- try:
- kefu = current_user.kefu_profile
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'})
-
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '客服状态异常'})
-
- # 2. 订单处理(事务内)
- try:
- with transaction.atomic():
- order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
-
- # 3. 校验订单状态
- if order.zhuangtai != 8:
- return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'})
-
- # 4. 校验必要字段
- dashou_id = order.jiedan_dashou_id
- dashou_fencheng = order.dashou_fencheng
- if not dashou_id:
- return Response({'code': 400, 'msg': '订单无接单打手'})
- if not dashou_fencheng or dashou_fencheng <= 0:
- return Response({'code': 400, 'msg': '打手分成金额无效'})
-
- # 5. 给打手结算
- try:
- dashou_user = UserMain.objects.get(yonghuid=dashou_id)
- dashou_profile = dashou_user.dashou_profile
- # 原子更新(避免并发重复加)
- dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1
- dashou_profile.yue = F('yue') + dashou_fencheng
- dashou_profile.zonge = F('zonge') + dashou_fencheng
- dashou_profile.jinrishouyi = F('jinrishouyi') + dashou_fencheng
- dashou_profile.jinyueshouyi = F('jinyueshouyi') + dashou_fencheng
- dashou_profile.save(update_fields=[
- 'chengjiaozongliang', 'yue', 'zonge',
- 'jinrishouyi', 'jinyueshouyi'
- ])
- except (UserMain.DoesNotExist, AttributeError):
- return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'})
-
- # 6. 如果是商家发单,更新商家成交订单数量
- if order.fadan_pingtai == 2:
- try:
- # 通过 DingdanShangjia 获取商家ID
- shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan'
- if shangjia_ext and shangjia_ext.shangjia_id:
- shangjia_user = UserMain.objects.get(yonghuid=shangjia_ext.shangjia_id)
- shangjia_profile = shangjia_user.shop_profile
- shangjia_profile.chengjiao = F('chengjiao') + 1
- shangjia_profile.save(update_fields=['chengjiao'])
-
- # ✅ 新方法(行为驱动,更安全)
- from houtai.utils import update_shangjia_daily
- update_shangjia_daily(
- yonghuid=shangjia_ext.shangjia_id,
- amount=Decimal(str(order.jine)),
- action=2 # 2 = 结算
- )
-
- except ObjectDoesNotExist:
- # 没有商家扩展记录则忽略
- pass
-
- # 7. 更新订单状态
- order.zhuangtai = 3
- order.clkf = phone
- order.save(update_fields=['zhuangtai', 'clkf'])
-
-
- if order.fadan_pingtai == 1:
-
- # ========== 新增:商品和店铺每日统计 ==========
- try:
- # 商品每日统计(只要有商品ID就执行)
- if order.shangpin_id:
- # 从扩展表获取已计算好的收益值(下单时已存储)
- pingtai_shouyi = Decimal('0.00')
- dianpu_shouyi = Decimal('0.00')
- if hasattr(order, 'pingtai_kuozhan'):
- pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00')
- dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
-
- update_shangpin_daily_stat(
- shangpin_id=order.shangpin_id,
- caozuo_leixing=2, # 2 = 结单
- dingdan_jiage=order.jine,
- pingtai_shouyi=pingtai_shouyi,
- dianpu_zongshouyi=dianpu_shouyi
- )
- logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
-
- # 店铺每日统计(仅当存在店铺ID时执行)
- dianpu_id = None
- if hasattr(order, 'pingtai_kuozhan'):
- dianpu_id = order.pingtai_kuozhan.dianpu_id
- if dianpu_id:
- dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
- update_dianpu_daily_stat(
- dianpu_id=dianpu_id,
- caozuo_leixing=2, # 2 = 结单
- dingdan_jiage=order.jine,
- dianpu_shouyi=dianpu_shouyi
- )
- logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
- except Exception as e:
- logger.error(f"商品/店铺统计更新失败: {e}")
- logger.error(traceback.format_exc())
- # 统计失败不影响主流程
- # =============================================
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.jiedan_dashou_id,
- amount=dashou_fencheng,
- action=2 # 2 表示成交(结算)
- )
- logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
- # 商家订单管事分红
- if order.fadan_pingtai == 2:
- from dingdan.utils import settle_shangjia_order_guanshi_fenhong
- settle_shangjia_order_guanshi_fenhong(order, dashou_id)
-
- # 8. 更新客服统计
- kefu.jinrichuli = F('jinrichuli') + 1
- kefu.jinyuechuli = F('jinyuechuli') + 1
- kefu.zongchuli = F('zongchuli') + 1
- kefu.save(update_fields=['jinrichuli', 'jinyuechuli', 'zongchuli'])
-
- return Response({'code': 0, 'msg': '强制结单成功'})
-
- except Dingdan.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'})
- except Exception as e:
- logger.error(f"强制结单异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-
-
-
-
-
-
-
-
-
-from django.db.models import Q, Sum
-
-
-# yonghu/views.py
-import logging
-import sys
-from django.db import DatabaseError
-from django.db.models import Q, Sum
-from yonghu.models import Tixianjilu, TixianShenheJilu
-
-kefu_txsh_list_logger = logging.getLogger('yonghu.kefu_txsh_list')
-if not kefu_txsh_list_logger.handlers:
- _console_handler = logging.StreamHandler(sys.stderr)
- _console_handler.setFormatter(logging.Formatter(
- '[%(asctime)s] %(levelname)s %(name)s: %(message)s'
- ))
- kefu_txsh_list_logger.addHandler(_console_handler)
- kefu_txsh_list_logger.setLevel(logging.DEBUG)
- kefu_txsh_list_logger.propagate = False
-
-_TIXIAN_LIST_BASE_FIELDS = (
- 'id', 'yonghuid', 'avatar', 'phone', 'nicheng', 'leixing',
- 'zhifu', 'skzhanghao', 'jine', 'zhuangtai', 'fangshi',
- 'shenheid', 'bhliyou', 'create_time', 'update_time',
-)
-_TIXIAN_LIST_AUDIT_FIELDS = ('shenhe_jilu_id', 'shenhe_danhao')
-
-
-class KefuWithdrawListView(APIView):
- """
- 客服获取提现审核列表接口
- 请求:POST /yonghu/kefu_txsh_list
- 参数:{
- "phone": "客服手机号",
- "page": 1,
- "page_size": 20,
- "status": 1(待审核)/2(已处理),
- "type": 1(打手佣金)/2(管事分红)/3(组长分红)/4(审核官分佣)/5(打手押金)/6(商家余额), 可选
- "payment": 1(微信)/2(支付宝) 可选,
- "search_uid": "用户ID搜索" 可选,
- "date": "YYYY-MM-DD" 可选,
- "min_amount": 0, # 最小提现金额(元)可选
- "max_amount": 999999, # 最大提现金额(元)可选
- "dakuan_mode": 1 # 打款方式:1手动打款 2自动打款,可选
- }
- 认证:JWT
- 返回:{
- "code": 0,
- "data": {
- "list": [提现记录],
- "total": 总数,
- "stats": {
- "total": 总记录数,
- "awaiting": 待审核数,
- "processed": 已处理数,
- "total_amount": "总金额(元)"
- }
- }
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def _safe_request_params(self, request):
- try:
- return dict(request.data)
- except Exception:
- return {'raw': str(request.data)}
-
- def _shenhe_jilu_field_available(self):
- """探测 shenhe_jilu_id 字段是否已在数据库中就绪"""
- try:
- Tixianjilu.objects.values_list('shenhe_jilu_id', flat=True).first()
- return True
- except DatabaseError as e:
- kefu_txsh_list_logger.error(
- 'kefu_txsh_list: shenhe_jilu_id 字段不可用,将按旧表结构查询: %s', e,
- exc_info=True,
- )
- return False
-
- def _list_queryset(self, q, shenhe_field_ok):
- """字段未迁移时 only 旧字段,避免 SELECT 不存在的列导致 500"""
- fields = _TIXIAN_LIST_BASE_FIELDS + (
- _TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else ()
- )
- return Tixianjilu.objects.filter(q).only(*fields)
-
- def _resolve_row_dakuan_mode(self, item, audit_mode_map, shenhe_field_ok):
- """无关联审核或审核表未标记自动 → 默认手动(1)"""
- if not shenhe_field_ok:
- return 1
- sid = getattr(item, 'shenhe_jilu_id', None)
- if not sid:
- return 1
- if audit_mode_map.get(sid) == 2:
- return 2
- return 1
-
- def post(self, request):
- dakuan_mode_filter = request.data.get('dakuan_mode')
- req_params = self._safe_request_params(request)
- kefu_txsh_list_logger.info('kefu_txsh_list 请求开始 params=%s', req_params)
-
- try:
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 20))
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
-
- if page < 1:
- page = 1
- if page_size < 1 or page_size > 100:
- page_size = min(max(page_size, 1), 100)
-
- status_filter = request.data.get('status')
- type_filter = request.data.get('type')
- payment_filter = request.data.get('payment')
- search_uid = (request.data.get('search_uid') or '').strip()
- date_filter = (request.data.get('date') or '').strip()
- min_amount = request.data.get('min_amount')
- max_amount = request.data.get('max_amount')
-
- phone = (request.data.get('phone') or '').strip()
- if not phone:
- return Response({'code': 401, 'msg': '缺少客服手机号'}, status=status.HTTP_401_UNAUTHORIZED)
-
- kefu, permissions = verify_kefu_permission(request, phone)
- if kefu is None:
- return permissions
- if '005bb' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
-
- q = Q()
-
- try:
- status_val = int(status_filter) if status_filter is not None and str(status_filter).strip() != '' else None
- except (TypeError, ValueError):
- status_val = None
-
- try:
- dakuan_mode_val = int(dakuan_mode_filter) if dakuan_mode_filter is not None and str(dakuan_mode_filter).strip() != '' else None
- except (TypeError, ValueError):
- dakuan_mode_val = None
-
- # 未传打款方式参数 → 默认手动打款(1)
- effective_dakuan = dakuan_mode_val if dakuan_mode_val in (1, 2) else 1
-
- if status_val == 1:
- q &= Q(zhuangtai=1)
- elif status_val == 2:
- if effective_dakuan == 2:
- q &= Q(zhuangtai__in=[2, 3, 4, 5, 6])
- else:
- q &= Q(zhuangtai__in=[2, 3])
-
- if type_filter is not None and str(type_filter).strip() != '':
- try:
- type_val = int(type_filter)
- if type_val in [1, 2, 3, 4, 5, 6]:
- q &= Q(leixing=type_val)
- except (TypeError, ValueError):
- pass
-
- if payment_filter is not None and str(payment_filter).strip() != '':
- try:
- payment_val = int(payment_filter)
- if payment_val in [1, 2]:
- q &= Q(fangshi=payment_val)
- except (TypeError, ValueError):
- pass
-
- if search_uid:
- q &= Q(yonghuid=search_uid)
-
- if date_filter:
- q &= Q(update_time__date=date_filter)
-
- if min_amount is not None and str(min_amount).strip() != '':
- try:
- q &= Q(jine__gte=float(min_amount))
- except (TypeError, ValueError):
- pass
- if max_amount is not None and str(max_amount).strip() != '':
- try:
- q &= Q(jine__lte=float(max_amount))
- except (TypeError, ValueError):
- pass
-
- shenhe_field_ok = self._shenhe_jilu_field_available()
- if shenhe_field_ok:
- if effective_dakuan == 2:
- q &= Q(shenhe_jilu_id__isnull=False)
- else:
- q &= Q(shenhe_jilu_id__isnull=True)
- elif effective_dakuan == 2:
- kefu_txsh_list_logger.warning('kefu_txsh_list: 自动打款筛选不可用,返回空列表')
- return Response({
- 'code': 0,
- 'data': {
- 'list': [],
- 'total': 0,
- 'stats': {'total': 0, 'awaiting': 0, 'processed': 0, 'total_amount': '0.00'},
- },
- })
-
- processed_statuses = [2, 3, 4, 5, 6] if effective_dakuan == 2 else [2, 3]
-
- base_qs = self._list_queryset(q, shenhe_field_ok)
- total_count = base_qs.count()
- awaiting_count = self._list_queryset(q & Q(zhuangtai=1), shenhe_field_ok).count()
- processed_count = self._list_queryset(
- q & Q(zhuangtai__in=processed_statuses), shenhe_field_ok,
- ).count()
- agg = base_qs.aggregate(total=Sum('jine'))
- total_amount = agg.get('total') or 0
-
- stats = {
- 'total': total_count,
- 'awaiting': awaiting_count,
- 'processed': processed_count,
- 'total_amount': str(round(float(total_amount), 2)),
- }
-
- queryset = base_qs.order_by('create_time')
- total = queryset.count()
- records = list(queryset[(page - 1) * page_size: page * page_size])
-
- audit_ids = [
- getattr(item, 'shenhe_jilu_id', None)
- for item in records if getattr(item, 'shenhe_jilu_id', None)
- ]
- audit_meta_map = {}
- if audit_ids:
- try:
- from .tixian_shenhe_services import load_audit_meta_map
- audit_meta_map = load_audit_meta_map(audit_ids)
- except Exception as e:
- kefu_txsh_list_logger.error(
- 'kefu_txsh_list: 查询审核表信息失败: %s', e,
- exc_info=True,
- )
-
- audit_mode_map = {
- aid: meta.get('dakuan_mode', 2) for aid, meta in audit_meta_map.items()
- }
- list_data = []
- for item in records:
- shenhe_jilu_id = getattr(item, 'shenhe_jilu_id', None) if shenhe_field_ok else None
- shenhe_danhao = (getattr(item, 'shenhe_danhao', None) or '') if shenhe_field_ok else ''
- if shenhe_jilu_id and not shenhe_danhao:
- shenhe_danhao = audit_meta_map.get(shenhe_jilu_id, {}).get('shenhe_danhao', '')
- list_data.append({
- 'id': item.id,
- 'shenhe_jilu_id': shenhe_jilu_id,
- 'shenhe_danhao': shenhe_danhao,
- 'yonghuid': item.yonghuid,
- 'avatar': item.avatar or '',
- 'phone': item.phone or '',
- 'nicheng': item.nicheng or '',
- 'leixing': item.leixing,
- 'zhifu': item.zhifu or '',
- 'skzhanghao': item.skzhanghao or '',
- 'jine': str(item.jine) if item.jine is not None else '0.00',
- 'zhuangtai': item.zhuangtai,
- 'fangshi': item.fangshi,
- 'dakuan_mode': self._resolve_row_dakuan_mode(item, audit_mode_map, shenhe_field_ok),
- 'shenheid': item.shenheid or '',
- 'bhliyou': item.bhliyou or '',
- 'create_time': item.create_time.strftime('%Y-%m-%d %H:%M:%S') if item.create_time else '',
- 'update_time': item.update_time.strftime('%Y-%m-%d %H:%M:%S') if item.update_time else '',
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': list_data,
- 'total': total,
- 'stats': stats,
- },
- })
-
- except DatabaseError as e:
- kefu_txsh_list_logger.error(
- 'kefu_txsh_list 数据库错误 dakuan_mode=%s params=%s err=%s',
- dakuan_mode_filter, req_params, e,
- exc_info=True,
- )
- return Response({
- 'code': 500,
- 'msg': '数据库查询失败,请确认已执行 python manage.py migrate yonghu',
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
- except Exception as e:
- kefu_txsh_list_logger.error(
- 'kefu_txsh_list 接口异常 dakuan_mode=%s params=%s err=%s',
- dakuan_mode_filter, req_params, e,
- exc_info=True,
- )
- return Response({
- 'code': 500,
- 'msg': '获取提现列表失败',
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-import logging
-from decimal import Decimal
-from django.core.exceptions import ObjectDoesNotExist
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-
-from yonghu.models import UserMain, UserKefu, Tixianjilu, UserDashou, UserGuanshi
-
-logger = logging.getLogger(__name__)
-
-class KefuWithdrawDetailView(APIView):
- """
- 客服获取提现详情接口
- 请求:POST /yonghu/kefu_txsh_detail
- 参数:{
- "phone": "客服手机号",
- "tixian_id": "提现记录ID"
- }
- 认证:JWT
- 返回:code=0 + 提现记录详情 + 用户扩展信息
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- phone = request.data.get('phone', '').strip()
- tixian_id = request.data.get('tixian_id')
-
- if not phone or not tixian_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 客服身份验证
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.user_type != 'kefu':
- logger.warning(f"用户类型非客服: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- kefu = current_user.kefu_profile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 查询提现记录
- try:
- record = Tixianjilu.objects.get(id=tixian_id)
- except Tixianjilu.DoesNotExist:
- return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 查询用户主表
- try:
- user = UserMain.objects.get(yonghuid=record.yonghuid)
- except UserMain.DoesNotExist:
- user = None
-
- # 构建基础返回数据(字段必须与前端一致)
- data = {
- 'id': record.id,
- 'yonghuid': record.yonghuid,
- 'avatar': record.avatar or '',
- 'phone': record.phone or '',
- 'nicheng': record.nicheng or '',
- 'leixing': record.leixing, # 1打手 2管事
- 'zhifu': record.zhifu or '',
- 'skzhanghao': record.skzhanghao or '',
- 'jine': str(record.jine) if record.jine else '0.00',
- 'zhuangtai': record.zhuangtai,
- 'fangshi': record.fangshi,
- 'shenheid': record.shenheid or '',
- 'bhliyou': record.bhliyou or '',
- 'create_time': record.create_time,
- 'update_time': record.update_time,
- }
-
- # 根据提现类型添加扩展信息
- if user:
- if record.leixing == 1: # 打手
- try:
- dashou = UserDashou.objects.get(user=user)
- # 计算成交率和退款率
- total = dashou.jiedanzongliang or 0
- completed = dashou.chengjiaozongliang or 0
- refund = dashou.tuikuanliang or 0
- completion_rate = round((completed / total * 100) if total > 0 else 0)
- refund_rate = round((refund / completed * 100) if completed > 0 else 0)
-
- data['dashouInfo'] = {
- 'zhuangtai': dashou.zhuangtai, # 工作状态
- 'zaixianzhuangtai': dashou.zaixianzhuangtai, # 在线状态
- 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, # 账号状态
- 'yue': float(dashou.yue) if dashou.yue else 0.00, # 可提现余额
- 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, # 接单总额
- 'jifen': dashou.jifen, # 积分
- 'jiedanzongliang': dashou.jiedanzongliang, # 接单总量
- 'chengjiaozongliang': dashou.chengjiaozongliang, # 成交总量
- 'tuikuanliang': dashou.tuikuanliang, # 退款总量
- 'completionRate': completion_rate,
- 'refundRate': refund_rate,
- }
- except ObjectDoesNotExist:
- data['dashouInfo'] = None
- elif record.leixing == 2: # 管事
- try:
- guanshi = UserGuanshi.objects.get(user=user)
- data['guanshiInfo'] = {
- 'zhuangtai': guanshi.zhuangtai, # 账号状态
- 'yue': float(guanshi.yue) if guanshi.yue else 0.00,
- 'yaogingshuliang': guanshi.yaogingshuliang, # 邀请打手总数
- 'jinrichongzhi': guanshi.jinrichongzhi, # 今日充值
- 'jinyuechongzhi': guanshi.jinyuechongzhi, # 今月充值
- 'chongzhifenrun': float(guanshi.chongzhifenrun) if guanshi.chongzhifenrun else 0.00,
- }
- except ObjectDoesNotExist:
- data['guanshiInfo'] = None
-
- return Response({'code': 0, 'data': data})
-
-import logging
-import json
-from decimal import Decimal
-from django.db import transaction
-from django.utils import timezone
-from django.core.exceptions import ObjectDoesNotExist
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-
-from yonghu.models import UserMain, UserKefu, Tixianjilu, UserDashou, UserGuanshi
-
-logger = logging.getLogger(__name__)
-
-
-class KefuWithdrawActionView(APIView):
- """
- 客服处理提现接口(同意/拒绝)—— 仅此接口处理后台审核打款
- POST /yonghu/kefu_txsh_action
-
- 单条:{ phone, action, tixian_id, yonghuid, leixing, reason? }
- 批量:{ phone, action, batch_list: [{tixian_id, yonghuid, leixing}, ...], reason? }
-
- 自动打款(dakuan_mode=2):同意→6待收款;拒绝→5+驳回原因+退还可到账金额(shijidaozhang),手续费不退,不更新平台收支/每日统计
- 手动打款(dakuan_mode=1):原逻辑不变
- 状态要求:Tixianjilu.zhuangtai=1 且 TixianShenheJilu.zhuangtai=1(审核中),其他状态一律拒绝
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def _verify_kefu(self, request, phone):
- if not phone:
- return None, Response({'code': 401, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST)
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- if current_user.user_type != 'kefu':
- return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- try:
- kefu = current_user.kefu_profile
- except ObjectDoesNotExist:
- return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- if kefu.zhuangtai != 1:
- return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- return current_user, None
-
- def _parse_items(self, request):
- """解析单条或 batch_list,返回 [{tixian_id, yonghuid, leixing}]"""
- batch_list = request.data.get('batch_list')
- if batch_list is not None:
- if not isinstance(batch_list, list) or len(batch_list) == 0:
- return None, Response({'code': 400, 'msg': 'batch_list 不能为空'}, status=status.HTTP_400_BAD_REQUEST)
- items = []
- for idx, row in enumerate(batch_list):
- if not isinstance(row, dict):
- return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 格式错误'}, status=status.HTTP_400_BAD_REQUEST)
- tid = row.get('tixian_id')
- yid = (row.get('yonghuid') or '').strip()
- lx = row.get('leixing')
- if not tid or not yid or lx is None:
- return None, Response(
- {'code': 400, 'msg': f'batch_list[{idx}] 缺少 tixian_id / yonghuid / leixing'},
- status=status.HTTP_400_BAD_REQUEST,
- )
- try:
- items.append({
- 'tixian_id': int(tid),
- 'yonghuid': yid,
- 'leixing': int(lx),
- })
- except (TypeError, ValueError):
- return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
- return items, None
-
- tixian_id = request.data.get('tixian_id')
- if not tixian_id:
- return None, Response({'code': 400, 'msg': '提现记录ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
- yonghuid = (request.data.get('yonghuid') or '').strip()
- leixing = request.data.get('leixing')
- try:
- item = {'tixian_id': int(tixian_id), 'yonghuid': yonghuid, 'leixing': int(leixing) if leixing is not None else None}
- except (TypeError, ValueError):
- return None, Response({'code': 400, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
- return [item], None
-
- def _process_auto_item(self, item, action, reason, kefu_user):
- """自动打款单条处理(须在 transaction.atomic 内调用)"""
- from .models import TixianShenheJilu
- from .tixian_shenhe_services import refund_balance, sync_audit_and_jilu_status
-
- tixian_id = item['tixian_id']
- req_yonghuid = item['yonghuid']
- req_leixing = item['leixing']
-
- if req_leixing not in [1, 2, 3, 4, 5, 6]:
- raise ValueError(f'提现记录{tixian_id}:提现类型无效')
-
- try:
- tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
- except Tixianjilu.DoesNotExist:
- raise ValueError(f'提现记录{tixian_id}不存在')
-
- if tixian.yonghuid != req_yonghuid:
- raise ValueError(f'提现记录{tixian_id}:用户ID不匹配')
- if tixian.leixing != req_leixing:
- raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
- if tixian.zhuangtai != 1:
- raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
- if not tixian.shenhe_jilu_id:
- raise ValueError(f'提现记录{tixian_id}:非自动打款审核记录')
-
- try:
- audit = TixianShenheJilu.objects.select_for_update().get(id=tixian.shenhe_jilu_id)
- except TixianShenheJilu.DoesNotExist:
- raise ValueError(f'提现记录{tixian_id}:审核记录不存在')
-
- if audit.yonghuid != req_yonghuid:
- raise ValueError(f'提现记录{tixian_id}:审核记录用户ID不匹配')
- if audit.leixing != req_leixing:
- raise ValueError(f'提现记录{tixian_id}:审核记录提现类型不匹配')
- if audit.zhuangtai != 1:
- raise ValueError(f'提现记录{tixian_id}:审核记录非审核中状态')
-
- try:
- user = UserMain.objects.select_for_update().get(yonghuid=req_yonghuid)
- except UserMain.DoesNotExist:
- raise ValueError(f'用户{req_yonghuid}不存在')
-
- if action == 1:
- sync_audit_and_jilu_status(audit, 6, shenhe_ren_id=kefu_user.yonghuid)
- tixian.shenheid = kefu_user.yonghuid
- tixian.save(update_fields=['shenheid', 'update_time'])
- else:
- sync_audit_and_jilu_status(
- audit, 5,
- bhliyou=reason,
- bo_hui_ren_id=kefu_user.yonghuid,
- )
- # 退还可到账金额,申请时扣的 shenqing_jine 中的手续费(shouxufei)不退
- refund_balance(user, audit.leixing, audit.shijidaozhang)
- tixian.shenheid = kefu_user.yonghuid
- tixian.bhliyou = reason
- tixian.save(update_fields=['shenheid', 'bhliyou', 'update_time'])
-
- def _process_manual_item(self, item, action, reason, phone):
- """手动打款单条处理(原逻辑,须在 transaction.atomic 内调用)"""
- tixian_id = item['tixian_id']
- try:
- tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
- except Tixianjilu.DoesNotExist:
- raise ValueError(f'提现记录{tixian_id}不存在')
-
- req_yonghuid = item.get('yonghuid')
- req_leixing = item.get('leixing')
- if req_yonghuid and tixian.yonghuid != req_yonghuid:
- raise ValueError(f'提现记录{tixian_id}:用户ID不匹配')
- if req_leixing is not None and tixian.leixing != req_leixing:
- raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
- if tixian.zhuangtai != 1:
- raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
-
- if getattr(tixian, 'shenhe_jilu_id', None):
- raise ValueError(f'提现记录{tixian_id}:自动打款请走自动流程')
-
- try:
- user = UserMain.objects.get(yonghuid=tixian.yonghuid)
- except UserMain.DoesNotExist:
- raise ValueError(f'用户{tixian.yonghuid}不存在')
-
- if action == 1:
- tixian.zhuangtai = 2
- try:
- from dingdan.utils import update_daily_payout
- update_daily_payout(tixian.jine)
- from peizhi.models import Szjilu
- szjilu, _ = Szjilu.objects.get_or_create(id=1)
- szjilu.zongsy -= tixian.jine
- szjilu.zongzc += tixian.jine
- szjilu.jrzc += tixian.jine
- szjilu.update_time = timezone.now()
- szjilu.save()
- except Exception as e:
- logger.error(f'更新收支记录失败: {e}')
- else:
- from .tixian_shenhe_services import refund_balance
- try:
- refund_balance(user, tixian.leixing, tixian.jine)
- except ValueError as e:
- raise ValueError(str(e))
- tixian.zhuangtai = 3
- tixian.bhliyou = reason
-
- tixian.shenheid = phone
- tixian.update_time = timezone.now()
- tixian.save()
-
- def post(self, request):
- logger.info(f'接收到提现处理请求,数据: {request.data}')
-
- phone = request.data.get('phone', '').strip()
- action_raw = request.data.get('action')
- reason = request.data.get('reason', '').strip()
-
- try:
- action = int(action_raw)
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': 'action 参数必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
- if action not in [1, 2]:
- return Response({'code': 400, 'msg': 'action 必须为1(同意)或2(拒绝)'}, status=status.HTTP_400_BAD_REQUEST)
- if action == 2 and not reason:
- return Response({'code': 400, 'msg': '拒绝提现时理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
-
- kefu_user, err_resp = self._verify_kefu(request, phone)
- if err_resp:
- return err_resp
-
- items, err_resp = self._parse_items(request)
- if err_resp:
- return err_resp
-
- is_batch = isinstance(request.data.get('batch_list'), list) and len(request.data.get('batch_list')) > 0
-
- try:
- with transaction.atomic():
- for item in items:
- tixian_id = item['tixian_id']
- try:
- tixian_probe = Tixianjilu.objects.select_for_update().get(id=tixian_id)
- except Tixianjilu.DoesNotExist:
- raise ValueError(f'提现记录{tixian_id}不存在')
-
- # 有关联审核记录(shenhe_jilu_id)即为 zddksh 自动打款,不依赖 dakuan_mode 字段
- is_auto = bool(getattr(tixian_probe, 'shenhe_jilu_id', None))
-
- if is_auto:
- self._process_auto_item(item, action, reason, kefu_user)
- else:
- if is_batch:
- raise ValueError(f'提现记录{tixian_id}:批量操作仅支持自动打款记录')
- self._process_manual_item(item, action, reason, phone)
-
- except ValueError as e:
- return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- logger.error(f'处理提现事务失败: {e}', exc_info=True)
- return Response({'code': 500, 'msg': f'处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- msg = '批量处理成功' if is_batch and len(items) > 1 else '处理成功'
- return Response({'code': 0, 'msg': msg, 'data': {'count': len(items)}})
-
-
-
-
-
-
-
-
-# dingdan/views.py
-import logging
-import uuid
-from django.db import transaction
-from django.core.exceptions import ObjectDoesNotExist
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-
-from yonghu.models import UserMain, UserKefu, UserDashou
-from dingdan.models import Dingdan, Chufajilu
-
-logger = logging.getLogger(__name__)
-
-
-
-
-
-class KefuPunishView(APIView):
- """
- 客服处罚打手接口
- 请求:POST /yonghu/kefuchufa
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "reason": "处罚原因(可选)"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- reason = request.data.get('reason', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.user_type != 'kefu':
- logger.warning(f"用户类型非客服: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- kefu = current_user.kefu_profile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单
- try:
- order = Dingdan.objects.get(dingdan_id=dingdan_id)
- except Dingdan.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 获取打手ID
- dashou_id = order.jiedan_dashou_id
- if not dashou_id:
- return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 查询打手
- try:
- dashou_user = UserMain.objects.get(yonghuid=dashou_id)
- dashou = dashou_user.dashou_profile
- except UserMain.DoesNotExist:
- return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND)
- except ObjectDoesNotExist:
- return Response({'code': 400, 'msg': '该用户不是打手'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 6. 使用事务
- with transaction.atomic():
- # 创建处罚记录(积分扣除5分)
- jifen = 5
- chufa = Chufajilu.objects.create(
- dashouid=dashou_id,
- qingqiuid=phone, # 客服手机号作为请求人
- cfliyou=reason or '',
- sqzhuangtai=0, # 待处理
- jifen=jifen,
- dingdan_id=dingdan_id,
- )
-
- # 扣除打手积分
- if dashou.jifen >= jifen:
- dashou.jifen -= jifen
- else:
- dashou.jifen = 0
- dashou.save()
-
- # 可选:更新订单的 clkf 字段记录处理客服
- order.clkf = phone
- order.save()
-
- return Response({'code': 0, 'msg': '处罚成功'})
-
-
-from django.db import transaction
-from django.contrib.auth.hashers import make_password
-
-class AdKftjView(APIView):
- """
- 添加客服接口
- 权限:管理员JWT
- 请求:POST /yonghu/adkftj
- 参数:{
- "zhanghao": "管理员账号",
- "phone": "客服手机号(必填)",
- "password": "密码(必填)",
- "erjimima": "二级密码(必填)",
- "nicheng": "昵称(可选)"
- }
- 返回:code=0
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 参数
- zhanghao = request.data.get('zhanghao', '').strip()
- phone = request.data.get('phone', '').strip()
- password = request.data.get('password', '').strip()
- erjimima = request.data.get('erjimima', '').strip()
- nicheng = request.data.get('nicheng', '').strip()
-
- if not all([zhanghao, phone, password, erjimima]):
- return Response({'code': 400, 'message': '手机号、密码、二级密码不能为空'})
-
- # 2. 管理员验证(同前)
- current_user = request.user
- if current_user.phone != zhanghao or current_user.user_type != 'admin':
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- try:
- admin_profile = current_user.admin_profile
- except:
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 检查手机号是否已被使用
- if UserMain.objects.filter(phone=phone).exists():
- return Response({'code': 400, 'message': '该手机号已被注册'})
-
- # 4. 生成唯一 yonghuid
- def generate_yonghuid():
- import random, time
- for _ in range(10):
- timestamp = str(int(time.time()))[-5:]
- rand = str(random.randint(0, 99)).zfill(2)
- uid = timestamp + rand
- if len(uid) == 7 and not UserMain.objects.filter(yonghuid=uid).exists():
- return uid
- raise Exception('无法生成唯一用户ID')
-
- with transaction.atomic():
- # 创建主表用户
- user = UserMain.objects.create(
- yonghuid=generate_yonghuid(),
- phone=phone,
- password=password,
- user_type='kefu',
- openid=f'kefu_{phone}' # 临时openid
- )
- # 创建客服扩展表
- kefu = UserKefu.objects.create(
- user=user,
- nicheng=nicheng or f'客服{user.yonghuid}',
- erjimima=erjimima, # 建议实际项目中加密存储
- zhuangtai=1,
- jinrichuli=0,
- jinyuechuli=0,
- zongchuli=0
- )
-
- return Response({'code': 0, 'message': '添加成功'})
-
-
-
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.parsers import JSONParser
-from django.db.models import Q
-import logging
-
-from yonghu.models import UserMain, UserKefu, AdminProfile
-
-logger = logging.getLogger(__name__)
-
-class AdKfglView(APIView):
- """
- 客服管理列表接口
- 权限:管理员JWT
- 请求:POST /yonghu/adkfgl
- 参数:{
- "zhanghao": "管理员账号",
- "keyword": "搜索关键词(可选,匹配客服ID或昵称)"
- }
- 返回:{
- "code": 0,
- "data": {
- "list": [客服对象...]
- }
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 参数获取
- zhanghao = request.data.get('zhanghao', '').strip()
- keyword = request.data.get('keyword', '').strip()
-
- if not zhanghao:
- return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 管理员身份验证
- current_user = request.user
- if not hasattr(current_user, 'phone') or str(current_user.phone) != zhanghao:
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.user_type != 'admin':
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- admin_profile = current_user.admin_profile
- except AttributeError:
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询所有客服(通过扩展表关联主表)
- queryset = UserKefu.objects.select_related('user').all()
-
- if keyword:
- queryset = queryset.filter(
- #Q(user__yonghuid__icontains=keyword) |
- Q(nicheng__icontains=keyword) |
- Q(user__phone__icontains=keyword)
- )
-
- # 4. 构建返回数据
- kefu_list = []
- for kefu in queryset:
- user = kefu.user
- kefu_list.append({
- 'yonghuid': user.phone,
- 'phone': user.phone or '',
- 'nicheng': kefu.nicheng or '',
- 'zhuangtai': kefu.zhuangtai,
- 'jinrichuli': kefu.jinrichuli,
- 'jinyuechuli': kefu.jinyuechuli,
- 'zongchuli': kefu.zongchuli,
- })
-
- return Response({
- 'code': 0,
- 'data': {'list': kefu_list}
- })
-
-
-
-
-
-class AdKfxgView(APIView):
- """
- 修改客服信息接口
- 权限:管理员JWT
- 请求:POST /yonghu/adkfxg
- 参数:{
- "zhanghao": "管理员账号",
- "uid": "客服ID (yonghuid)",
- "nicheng": "新昵称(可选)",
- "phone": "新手机号(可选)",
- "password": "新密码(可选)",
- "erjimima": "新二级密码(可选)",
- "zhuangtai": 0/1(可选)
- }
- 返回:code=0
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- zhanghao = request.data.get('zhanghao', '').strip()
- uid = request.data.get('uid', '').strip()
-
- if not zhanghao or not uid:
- return Response({'code': 400, 'message': '参数不完整'})
-
- # 管理员验证
- current_user = request.user
- if current_user.phone != zhanghao or current_user.user_type != 'admin':
- return Response({'code': 401, 'message': '身份验证失败'})
- try:
- admin_profile = current_user.admin_profile
- except:
- return Response({'code': 401, 'message': '身份验证失败'})
-
- # 查询客服
- try:
- user = UserMain.objects.get(phone=uid)
- kefu = user.kefu_profile
- except (UserMain.DoesNotExist, UserKefu.DoesNotExist):
- return Response({'code': 404, 'message': '客服不存在'})
-
- # 更新数据
- with transaction.atomic():
- # 更新主表字段
- if 'phone' in request.data:
- new_phone = request.data.get('phone').strip()
- if new_phone and UserMain.objects.filter(phone=new_phone).exclude(phone=uid).exists():
- return Response({'code': 400, 'message': '手机号已被使用'})
- user.phone = new_phone
- if 'password' in request.data:
- user.password = request.data['password']
- user.save()
-
- # 更新扩展表
- if 'nicheng' in request.data:
- kefu.nicheng = request.data['nicheng']
- if 'erjimima' in request.data:
- kefu.erjimima = request.data['erjimima']
- if 'zhuangtai' in request.data:
- kefu.zhuangtai = int(request.data['zhuangtai'])
- kefu.save()
-
- return Response({'code': 0, 'message': '修改成功'})
-
-
-
-
-
-from .models import (
- UserMain, UserBoss, UserDashou, UserGuanshi,
- UserZuzhang
-)
-
-
-
-class WechatLoginAndGuanshiRegisterView(APIView):
- """
- 未登录用户微信登录+管事注册一体化接口
- 路径: /api/yonghu/zuzhangyqmzc
- 方法: POST
- 权限: 允许所有
- 请求体: { "code": "微信登录code", "inviteCode": "组长邀请码" }
- 成功返回 code: 200,数据同 GuanshiRegisterView
- """
- permission_classes = [AllowAny]
-
- def post(self, request):
- code = request.data.get('code', '').strip()
- yaoqingma = request.data.get('inviteCode', '').strip()
-
- if not code:
- return Response({'code': 400, 'msg': '微信授权码不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- if not yaoqingma:
- return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- if len(yaoqingma) > 100:
- return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- wechat_data = self._get_wechat_openid(code)
- if not wechat_data or 'openid' not in wechat_data:
- error_msg = wechat_data.get('errmsg', '微信授权失败')
- return Response({'code': 400, 'msg': f'微信登录失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- openid = wechat_data['openid']
- kehuduan_ip = self._huoqu_kehuduan_ip(request)
-
- with transaction.atomic():
- user_main, created = UserMain.objects.select_for_update().get_or_create(
- openid=openid,
- defaults={
- 'yonghuid': self._shengcheng_yonghu_id(),
- 'user_type': 'normal',
- }
- )
- user_main.ip = kehuduan_ip
- user_main.last_login_time = timezone.now()
- user_main.save()
-
- if created:
- UserBoss.objects.create(user=user_main, nickname='微信用户')
-
- if hasattr(user_main, 'guanshi_profile'):
- return self._fanhui_xianyou_guanshi_info(user_main)
-
- try:
- zuzhang = UserZuzhang.objects.select_related('user').get(yaoqingma=yaoqingma)
- except UserZuzhang.DoesNotExist:
- return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND)
-
- if zuzhang.zhuangtai != 1:
- return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None},
- status=status.HTTP_403_FORBIDDEN)
-
- zuzhang_yonghuid = zuzhang.user.yonghuid
-
- guanshi_yaoqingma = chuangjianYaoqingma(str(user_main.yonghuid))
- guanshi_profile = UserGuanshi.objects.create(
- user=user_main,
- yaoqingma=guanshi_yaoqingma,
- yaoqingren=zuzhang_yonghuid,
- )
-
- UserZuzhang.objects.filter(pk=zuzhang.pk).update(
- yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
- )
-
- # ========== 新增:组长每日统计(邀请管事 action=1) ==========
- try:
- from houtai.utils import update_zuzhang_daily_by_action
- update_zuzhang_daily_by_action(
- yonghuid=zuzhang_yonghuid,
- action=1 # 1 = 邀请管事
- )
- logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}")
- except Exception as e:
- logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}")
-
-
- if not hasattr(user_main, 'dashou_profile'):
- dashou_profile = UserDashou.objects.create(
- user=user_main,
- nicheng='大神',
- chenghao='普通大神',
- yaoqingren=zuzhang_yonghuid,
- )
- else:
- dashou_profile = user_main.dashou_profile
-
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
-
- response_data = self._zhuangbei_fanhui_shuju(
- user_main, dashou_profile, guanshi_profile, token
- )
-
- return Response({'code': 0, 'msg': '登录并注册成功!', 'data': response_data})
-
- # ---------- 辅助方法(与接口1完全相同,完整列出以保证完整性) ----------
-
- def _get_wechat_openid(self, code):
- try:
- appid = getattr(settings, 'WEIXIN_APPID', '')
- secret = getattr(settings, 'WEIXIN_SECRET', '')
- if not appid or not secret:
- raise ValueError('微信配置未设置')
- url = 'https://api.weixin.qq.com/sns/jscode2session'
- params = {
- 'appid': appid,
- 'secret': secret,
- 'js_code': code,
- 'grant_type': 'authorization_code'
- }
- response = requests.get(url, params=params, timeout=10)
- result = response.json()
- if 'openid' in result:
- return result
- else:
- errcode = result.get('errcode', 'unknown')
- errmsg = result.get('errmsg', '未知错误')
- return {'errmsg': f'[{errcode}]{errmsg}'}
- except requests.exceptions.Timeout:
- logger.error('请求微信接口超时')
- return {'errmsg': '请求微信接口超时'}
- except Exception as e:
- logger.error(f'请求微信接口异常: {e}')
- return {'errmsg': f'请求微信接口异常: {str(e)}'}
-
- def _shengcheng_yonghu_id(self):
- for _ in range(10):
- timestamp_part = str(int(time.time()))[-5:].zfill(5)
- random_part = str(random.randint(0, 99)).zfill(2)
- user_id = timestamp_part + random_part
- if len(user_id) == 7 and user_id.isdigit():
- if not UserMain.objects.filter(yonghuid=user_id).exists():
- return user_id
- raise Exception('生成用户ID失败')
-
- def _huoqu_kehuduan_ip(self, request):
- 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
- ip = request.META.get('REMOTE_ADDR', '')
- return ip if ip else '0.0.0.0'
-
- def _huoqu_qun_peizhi(self):
- try:
- qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2])
- result = {
- 'dashouqun': '',
- 'dashouqunid': '',
- 'guanshiqun': '',
- 'guanshiqunid': ''
- }
- for config in qun_configs:
- if config.id == 1:
- result['dashouqun'] = config.neirong or ''
- result['dashouqunid'] = config.qunid or ''
- elif config.id == 2:
- result['guanshiqun'] = config.neirong or ''
- result['guanshiqunid'] = config.qunid or ''
- return result
- except Exception as e:
- logger.error(f"获取群配置失败: {e}")
- return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''}
-
- def _zhuangbei_dashou_fanhui(self, dashou_profile):
- data = {
- 'dashounicheng': dashou_profile.nicheng,
- 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
- 'yongjin': str(dashou_profile.yue),
- 'zonge': str(dashou_profile.zonge),
- 'yajin': str(dashou_profile.yajin),
- 'chenghao': dashou_profile.chenghao,
- 'jinfen': dashou_profile.jifen,
- 'chengjiaoliang': dashou_profile.chengjiaozongliang,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'dashouzhuangtai': dashou_profile.zhuangtai,
- }
- huiyuan_list = []
- try:
- records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid)
- for record in records:
- shifou_daoqi = record.jiance_shifou_daoqi()
- huiyuan_list.append({
- 'huiyuan_id': record.huiyuan_id,
- 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
- 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
- 'shifou_daoqi': shifou_daoqi,
- 'jieshao': record.jieshao
- })
- except Exception as e:
- logger.warning(f"获取会员列表失败: {e}")
- data['clumber'] = huiyuan_list
- return data
-
- def _fanhui_xianyou_guanshi_info(self, user_main):
- guanshi_profile = user_main.guanshi_profile
- dashou_profile = getattr(user_main, 'dashou_profile', None)
- group_info = self._huoqu_qun_peizhi()
- dashou_status = 1 if dashou_profile else 0
- shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0
- guanshi_status = 1
- try:
- nicheng = user_main.boss_profile.nickname or '微信用户'
- except:
- nicheng = '微信用户'
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
- data = {
- 'token': token,
- 'nicheng': nicheng,
- 'uid': user_main.yonghuid,
- 'touxiang': user_main.avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- **group_info,
- }
- guanshi_data = {
- 'gszhstatus': guanshi_profile.zhuangtai,
- 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
- 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
- 'fenyongtixian': str(guanshi_profile.yue),
- 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
- }
- data.update(guanshi_data)
- if dashou_profile:
- dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
- data.update(dashou_data)
- else:
- data.update({
- 'dashounicheng': '',
- 'zhanghaostatus': 0,
- 'yongjin': '0.00',
- 'zonge': '0.00',
- 'yajin': '0.00',
- 'chenghao': '',
- 'jinfen': 0,
- 'chengjiaoliang': 0,
- 'zaixianzhuangtai': 0,
- 'dashouzhuangtai': 0,
- 'clumber': []
- })
- return Response({'code': 0, 'msg': '您已是管事', 'data': data})
-
- def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token):
- try:
- nicheng = user_main.boss_profile.nickname or '微信用户'
- except:
- nicheng = '微信用户'
- dashou_status = 1
- shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0
- guanshi_status = 1
- group_info = self._huoqu_qun_peizhi()
- data = {
- 'token': token,
- 'nicheng': nicheng,
- 'uid': user_main.yonghuid,
- 'touxiang': user_main.avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- **group_info,
- }
- guanshi_data = {
- 'gszhstatus': guanshi_profile.zhuangtai,
- 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
- 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
- 'fenyongtixian': str(guanshi_profile.yue),
- 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
- }
- data.update(guanshi_data)
- dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
- data.update(dashou_data)
- return data
-
-
-
-
-
-
-
-from django.db import transaction, models
-
-
-
-
-
-
-
-
-class GuanshiRegisterView(APIView):
- """
- 已登录用户注册管事接口
- 路径: /api/yonghu/guanshizhuce
- 方法: POST
- 权限: JWT认证
- 请求体: { "inviteCode": "组长邀请码" }
- 成功返回 code: 200
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 获取并验证邀请码
- yaoqingma = request.data.get('inviteCode', '').strip()
- if not yaoqingma:
- return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- if len(yaoqingma) > 100:
- return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- current_user = request.user
-
- # 2. 检查用户是否已是管事
- if hasattr(current_user, 'guanshi_profile'):
- # 用户已是管事,直接返回现有信息
- return self._fanhui_xianyou_guanshi_info(current_user)
-
- # 3. 验证邀请码对应的组长
- try:
- zuzhang = UserZuzhang.objects.select_related('user').get(yaoqingma=yaoqingma)
- except UserZuzhang.DoesNotExist:
- return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND)
-
- if zuzhang.zhuangtai != 1:
- return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None},
- status=status.HTTP_403_FORBIDDEN)
-
- zuzhang_yonghuid = zuzhang.user.yonghuid
-
- # 4. 事务内创建管事实体和打手(如果需要)
- with transaction.atomic():
- # 创建管事扩展表
- guanshi_yaoqingma = chuangjianYaoqingma(str(current_user.yonghuid))
- guanshi_profile = UserGuanshi.objects.create(
- user=current_user,
- yaoqingma=guanshi_yaoqingma,
- yaoqingren=zuzhang_yonghuid,
- )
-
- # 更新组长的邀请总数
- UserZuzhang.objects.filter(pk=zuzhang.pk).update(
- yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
- )
-
- # ========== 新增:组长每日统计(邀请管事 action=1) ==========
- try:
- from houtai.utils import update_zuzhang_daily_by_action
- update_zuzhang_daily_by_action(
- yonghuid=zuzhang_yonghuid,
- action=1 # 1 = 邀请管事
- )
- logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}")
- except Exception as e:
- logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}")
-
- # 检查用户是否已是打手,如果不是则创建
- if not hasattr(current_user, 'dashou_profile'):
- dashou_profile = UserDashou.objects.create(
- user=current_user,
- nicheng='大手子',
- chenghao='普通大手',
- yaoqingren=zuzhang_yonghuid,
- )
- else:
- dashou_profile = current_user.dashou_profile
-
- # 5. 生成新token
- refresh = RefreshToken.for_user(current_user)
- token = str(refresh.access_token)
-
- # 6. 准备返回数据(包含所有字段)
- response_data = self._zhuangbei_fanhui_shuju(
- current_user, dashou_profile, guanshi_profile, token
- )
-
- return Response({'code': 200, 'msg': '注册成功!', 'data': response_data})
-
- # ---------- 辅助方法 ----------
-
- def _huoqu_qun_peizhi(self):
- """获取群配置信息"""
- try:
- qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2])
- result = {
- 'dashouqun': '',
- 'dashouqunid': '',
- 'guanshiqun': '',
- 'guanshiqunid': ''
- }
- for config in qun_configs:
- if config.id == 1:
- result['dashouqun'] = config.neirong or ''
- result['dashouqunid'] = config.qunid or ''
- elif config.id == 2:
- result['guanshiqun'] = config.neirong or ''
- result['guanshiqunid'] = config.qunid or ''
- return result
- except Exception as e:
- logger.error(f"获取群配置失败: {e}")
- return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''}
-
- def _zhuangbei_dashou_fanhui(self, dashou_profile):
- """组装打手信息(字段名与前端完全匹配)"""
- data = {
- 'dashounicheng': dashou_profile.nicheng,
- 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
- 'yongjin': str(dashou_profile.yue),
- 'zonge': str(dashou_profile.zonge),
- 'yajin': str(dashou_profile.yajin),
- 'chenghao': dashou_profile.chenghao,
- 'jinfen': dashou_profile.jifen,
- 'chengjiaoliang': dashou_profile.chengjiaozongliang,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'dashouzhuangtai': dashou_profile.zhuangtai,
- }
-
- # 会员列表
- huiyuan_list = []
- try:
- records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid)
- for record in records:
- shifou_daoqi = record.jiance_shifou_daoqi()
- huiyuan_list.append({
- 'huiyuan_id': record.huiyuan_id,
- 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
- 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
- 'shifou_daoqi': shifou_daoqi,
- 'jieshao': record.jieshao
- })
- except Exception as e:
- logger.warning(f"获取会员列表失败: {e}")
- data['clumber'] = huiyuan_list
- return data
-
- def _fanhui_xianyou_guanshi_info(self, user_main):
- """用户已是管事时的返回"""
- guanshi_profile = user_main.guanshi_profile
- dashou_profile = getattr(user_main, 'dashou_profile', None)
-
- group_info = self._huoqu_qun_peizhi()
-
- dashou_status = 1 if dashou_profile else 0
- shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0
- guanshi_status = 1
-
- try:
- nicheng = user_main.boss_profile.nickname or '微信用户'
- except:
- nicheng = '微信用户'
-
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
-
- data = {
- 'token': token,
- 'nicheng': nicheng,
- 'uid': user_main.yonghuid,
- 'touxiang': user_main.avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- **group_info,
- }
-
- # 管事信息
- guanshi_data = {
- 'gszhstatus': guanshi_profile.zhuangtai,
- 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
- 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
- 'fenyongtixian': str(guanshi_profile.yue),
- 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
- }
- data.update(guanshi_data)
-
- if dashou_profile:
- dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
- data.update(dashou_data)
- else:
- data.update({
- 'dashounicheng': '',
- 'zhanghaostatus': 0,
- 'yongjin': '0.00',
- 'zonge': '0.00',
- 'yajin': '0.00',
- 'chenghao': '',
- 'jinfen': 0,
- 'chengjiaoliang': 0,
- 'zaixianzhuangtai': 0,
- 'dashouzhuangtai': 0,
- 'clumber': []
- })
-
- return Response({'code': 200, 'msg': '您已是管事', 'data': data})
-
- def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token):
- """组装完整返回数据(新注册)"""
- try:
- nicheng = user_main.boss_profile.nickname or '微信用户'
- except:
- nicheng = '微信用户'
-
- dashou_status = 1
- shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0
- guanshi_status = 1
-
- group_info = self._huoqu_qun_peizhi()
-
- data = {
- 'token': token,
- 'nicheng': nicheng,
- 'uid': user_main.yonghuid,
- 'touxiang': user_main.avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- **group_info,
- }
-
- # 管事信息
- guanshi_data = {
- 'gszhstatus': guanshi_profile.zhuangtai,
- 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
- 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
- 'fenyongtixian': str(guanshi_profile.yue),
- 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
- }
- data.update(guanshi_data)
-
- dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
- data.update(dashou_data)
-
- return data
-
-
-
-
-
-
-
-
-
-from rest_framework_simplejwt.authentication import JWTAuthentication
-from django.db import IntegrityError
-
-
-
-class ZuzhangZhuceView(APIView):
- """
- 组长注册/激活接口
- POST /yonghu/zuzhangzhuce
- 请求体: {"inviteCode": "xxx"}
- 返回:
- {
- "code": 200,
- "msg": "success",
- "data": {
- "ketixian": "0.00", # 可提现金额
- "guanshiCount": 0, # 邀请的管事数量
- "fenhongZonge": "0.00" # 分红总额
- }
- }
- 或错误码
- """
-
- permission_classes = [IsAuthenticated]
-
- # 硬编码激活码(注意转义特殊字符)
- ACTIVATION_CODE = "237huwehdw77e777eyw224fr"
-
- def post(self, request):
- user = request.user
- invite_code = request.data.get('inviteCode', '').strip()
-
- if not invite_code:
- return Response({'code': 400, 'msg': '邀请码不能为空'}, status=400)
-
- # 检查是否已是组长
- try:
- zuzhang = UserZuzhang.objects.select_related('user').get(user=user)
- # 已注册,直接返回数据
- data = self._build_response_data(zuzhang, user)
- return Response({'code':200, 'msg': '已注册', 'data': data})
- except UserZuzhang.DoesNotExist:
- pass
-
- # 比对激活码
- if invite_code != self.ACTIVATION_CODE:
- return Response({'code': 400, 'msg': '邀请码无效'}, status=400)
-
- # 生成唯一组长邀请码:时间戳 + 用户ID后6位 + 随机字符
- yaoqingma = self._generate_unique_yaoqingma(user.yonghuid)
-
- # 创建组长扩展记录
- try:
- with transaction.atomic():
- zuzhang = UserZuzhang.objects.create(
- user=user,
- yaoqingma=yaoqingma,
- fenyong_zonge=0.00,
- ketixian_jine=0.00,
- yaoqing_zongshu=0,
- jinri_fenyong=0.00,
- jinyue_fenyong=0.00,
- zhuangtai=1,
- jinri_tixian=0.00,
- ewai_tixian_xiane=0.00,
- kaioi_ewai_tixian=False,
- kaioi_ewai_fenhong=False,
- ewai_fenhong_jine=0.00
- )
- except IntegrityError:
- # 极低概率重复,重试一次
- yaoqingma = self._generate_unique_yaoqingma(user.yonghuid, retry=True)
- zuzhang = UserZuzhang.objects.create(
- user=user,
- yaoqingma=yaoqingma,
- # 其他字段默认同上
- fenyong_zonge=0.00,
- ketixian_jine=0.00,
- yaoqing_zongshu=0,
- jinri_fenyong=0.00,
- jinyue_fenyong=0.00,
- zhuangtai=1,
- jinri_tixian=0.00,
- ewai_tixian_xiane=0.00,
- kaioi_ewai_tixian=False,
- kaioi_ewai_fenhong=False,
- ewai_fenhong_jine=0.00
- )
-
- data = self._build_response_data(zuzhang, user)
- return Response({'code':200, 'msg': '注册成功', 'data': data})
-
- def _generate_unique_yaoqingma(self, yonghuid, retry=False):
- """生成唯一邀请码:时间戳+用户ID后6位+随机2位字符"""
- import random, string
- timestamp = str(int(time.time()))[-6:] # 取后6位
- uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0')
- rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2))
- base = f"Z{timestamp}{uid_part}{rand_str}"
- # 确保唯一性(若已存在则追加随机数,最多尝试3次)
- for _ in range(3):
- if not UserZuzhang.objects.filter(yaoqingma=base).exists():
- return base
- base = base + random.choice(string.digits)
- # 极端情况,再附加时间戳毫秒
- millis = str(time.time()).replace('.', '')[-4:]
- return base + millis
-
- def _build_response_data(self, zuzhang, user):
- """组装前端所需数据"""
- # 计算邀请的管事数量(通过UserGuanshi表的yaoqingren字段关联到user的yonghuid)
- guanshi_count = UserGuanshi.objects.filter(yaoqingren=user.yonghuid).count()
- return {
- 'ketixian': str(zuzhang.ketixian_jine), # 可提现余额
- 'guanshiCount': guanshi_count, # 邀请管事数
- 'fenhongZonge': str(zuzhang.fenyong_zonge) # 分红总额
- }
-
-
-
-class ZuzhangXinxiView(APIView):
- """
- 组长信息查询接口
- POST /yonghu/zuzhangxinxi
- 请求体: 无
- 返回: 同注册接口成功返回格式
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- user = request.user
- try:
- zuzhang = UserZuzhang.objects.get(user=user)
- except UserZuzhang.DoesNotExist:
- return Response({'code': 404, 'msg': '用户不是组长'}, status=404)
-
- guanshi_count = UserGuanshi.objects.filter(yaoqingren=user.yonghuid).count()
- data = {
- 'ketixian': str(zuzhang.ketixian_jine),
- 'guanshiCount': guanshi_count,
- 'fenhongZonge': str(zuzhang.fenyong_zonge)
- }
- return Response({'code': 200, 'msg': 'success', 'data': data})
-
-
-
-
-
-# apps/yonghu/views_dashou_jianquan.py
-"""
-打手 IM 消息权限鉴权接口
-URL: /yonghu/dshqltqx
-方法: POST
-认证: JWT Token (request.user)
-
-鉴权逻辑(满足任意一条即通过):
- 1. 押金 >= 5 元 → 通过
- 2. 拥有未过期会员 → 通过
- 3. 存在订单状态为 2 或 8 的订单(作为接单打手) → 通过
- 以上均不满足 → 拒绝
-"""
-
-
-class DashouJianquanView(APIView):
- """打手消息权限鉴权"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # ========== 1. 通过 JWT 获取当前登录用户 ==========
- user = request.user # UserMain 实例
-
- # ========== 2. 检查是否为已注册打手 ==========
- try:
- dashou = user.dashou_profile # OneToOneField 反向查询
- except UserDashou.DoesNotExist:
- return Response({
- 'code': 0,
- 'allow': 0,
- 'msg': '您尚未注册打手身份,请先注册'
- })
-
- # ========== 3. 条件一:押金 >= 5 元 ==========
- if dashou.yajin and dashou.yajin >= 5:
- return Response({
- 'code': 0,
- 'allow': 1,
- 'msg': '鉴权通过(押金满足条件)'
- })
-
- # ========== 4. 条件二:存在未过期会员 ==========
- # 高效查询:利用 yonghu_id + huiyuan_zhuangtai 复合索引
- huiyuan_list = Huiyuangoumai.objects.filter(
- yonghu_id=user.yonghuid,
- huiyuan_zhuangtai=1 # 只查状态为"可用"的,减少内存遍历
- ).only('id', 'daoqi_time', 'huiyuan_zhuangtai')
-
- for hy in huiyuan_list:
- if not hy.jiance_shifou_daoqi(): # 未过期返回 False
- return Response({
- 'code': 0,
- 'allow': 1,
- 'msg': '鉴权通过(会员未过期)'
- })
-
- # ========== 5. 条件三:存在订单状态为 2 或 8 的订单 ==========
- # 利用 jiedan_dashou_id + zhuangtai 复合索引,只查 1 条
- has_valid_order = Dingdan.objects.filter(
- jiedan_dashou_id=user.yonghuid,
- zhuangtai__in=[2, 8]
- ).only('id').exists() # exists() 高性能判断,只发 SELECT 1 LIMIT 1
-
- if has_valid_order:
- return Response({
- 'code': 0,
- 'allow': 1,
- 'msg': '鉴权通过(存在有效订单)'
- })
-
- # ========== 6. 以上均不满足 → 拒绝 ==========
- return Response({
- 'code': 0,
- 'allow': 0,
- 'msg': '消息权限不足,请充值会员或联系管理员'
- })
-
-
-
-# apps/yonghu/views_laoban_jianquan.py
-"""
-老板(普通用户) IM 消息权限鉴权接口
-URL: /yonghu/lbhqltqx
-方法: POST
-认证: JWT Token (request.user)
-
-鉴权逻辑:
- 查询平台订单扩展表(DingdanPingtai),
- 通过关联的订单主表(Dingdan)查找该老板的订单,
- 只要存在任意一条订单状态在 1~8 范围内 → 通过
-"""
-
-
-
-class LaobanJianquanView(APIView):
- """老板消息权限鉴权"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # ========== 1. 通过 JWT 获取当前登录用户 ==========
- user = request.user # UserMain 实例
-
- # ========== 2. 高效查询平台订单扩展表 ==========
- # 利用 DingdanPingtai.laoban_id 索引 + Dingdan.zhuangtai 索引
- # 只查 1 条,用 exists() 实现最高性能
- has_valid_order = DingdanPingtai.objects.filter(
- laoban_id=user.yonghuid
- ).filter(
- dingdan__zhuangtai__in=[1, 2, 3, 4, 5, 6, 7, 8]
- ).only('id').exists()
-
- if has_valid_order:
- return Response({
- 'code': 0,
- 'allow': 1,
- 'msg': '鉴权通过'
- })
-
- return Response({
- 'code': 0,
- 'allow': 0,
- 'msg': '您暂无订单记录,消息功能暂不可用'
- })
-
-
-
-
-
-
-
-# views.py (在处罚相关app的views中)
-import logging
-from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from rest_framework.views import APIView
-from django.db.models import Count, Q, Case, When, IntegerField
-from dingdan.models import Fadan
-from dingdan.models import FadanShensuTupian
-
-logger = logging.getLogger('xiaochengxu')
-
-class FaKuanJiFenTongJiView(APIView):
- """
- 获取罚款与积分处罚统计总览
- 路径: POST /yonghu/cffktjhq
- """
- permission_classes = [IsAuthenticated, DashouPermission]
-
- def post(self, request):
- try:
- user_main = request.user
- yonghuid = user_main.yonghuid
-
- # ================= 罚款统计 =================
- fadan_stats = Fadan.objects.filter(beichufa_id=yonghuid).aggregate(
- total=Count('id'),
- daijiaona=Count('id', filter=Q(zhuangtai=1)),
- shensuzhong=Count('id', filter=Q(zhuangtai=3)),
- yijiaona=Count('id', filter=Q(zhuangtai=2)),
- yibohui=Count('id', filter=Q(zhuangtai=4)),
- )
-
- # ================= 积分处罚统计 =================
- jifen_stats = Chufajilu.objects.filter(dashouid=yonghuid).aggregate(
- total=Count('id'),
- daichuli=Count('id', filter=Q(sqzhuangtai=0) | Q(sqzhuangtai=3)),
- yichuli=Count('id', filter=Q(sqzhuangtai=1) | Q(sqzhuangtai=2)),
- # 细分
- daichuli_0=Count('id', filter=Q(sqzhuangtai=0)),
- shensuzhong_3=Count('id', filter=Q(sqzhuangtai=3)),
- yichufa_1=Count('id', filter=Q(sqzhuangtai=1)),
- yibohui_2=Count('id', filter=Q(sqzhuangtai=2)),
- )
-
- data = {
- "fakuan": {
- "total": fadan_stats['total'],
- "daijiaona": fadan_stats['daijiaona'],
- "shensuzhong": fadan_stats['shensuzhong'],
- "yijiaona": fadan_stats['yijiaona'],
- "yibohui": fadan_stats['yibohui'],
- },
- "jifen": {
- "total": jifen_stats['total'],
- "daichuli": jifen_stats['daichuli'],
- "yichuli": jifen_stats['yichuli'],
- "daichuli_0": jifen_stats['daichuli_0'],
- "shensuzhong_3": jifen_stats['shensuzhong_3'],
- "yichufa_1": jifen_stats['yichufa_1'],
- "yibohui_2": jifen_stats['yibohui_2'],
- }
- }
-
- return Response({'code': 0, 'msg': '获取统计成功', 'data': data})
-
- except Exception as e:
- logger.error(f"获取统计异常 {yonghuid}: {e}", exc_info=True)
- return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
-
-class DaShouFaKuanLieBiaoView(APIView):
- """
- 打手罚款列表接口
- 路径:POST /yonghu/dsfklbhq
- 参数:
- page: 页码(默认1)
- page_size: 每页条数(默认10,最大50)
- zhuangtai: 状态筛选(1待缴纳/2已缴纳/3申诉中/4申诉成功,0或不传表示全部)
- sousuo_dingdan_id: 订单号模糊搜索
- 返回:{
- list: [{
- id, beichufa_id, guanliandingdan_id, chufaliyou, fakuanjine, zhuangtai,
- yingxiang_qiangdan, shensuliyou, bohuiliyou,
- zhengju_tupian: [...], // 证据图片(yongtu=1)
- shensu_tupian: [...], // 申诉图片(yongtu=2)
- create_time, update_time
- }],
- has_more, page, page_size
- }
- """
- permission_classes = [IsAuthenticated, DashouPermission]
-
- def post(self, request):
- try:
- user_main = request.user
- yonghuid = user_main.yonghuid
-
- # 获取分页参数
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 10))
- if page_size > 50:
- page_size = 50
- if page_size < 1:
- page_size = 10
-
- # 状态筛选
- zhuangtai = request.data.get('zhuangtai')
- # 订单号搜索
- sousuo_dingdan_id = request.data.get('sousuo_dingdan_id', '').strip()
-
- # 基础查询:当前用户的罚单
- qs = Fadan.objects.filter(beichufa_id=yonghuid)
-
- # 状态筛选
- if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]:
- qs = qs.filter(zhuangtai=int(zhuangtai))
-
- # 订单号模糊搜索
- if sousuo_dingdan_id:
- qs = qs.filter(guanliandingdan_id__icontains=sousuo_dingdan_id)
-
- # 按创建时间倒序
- qs = qs.order_by('-create_time')
-
- # 分页
- paginator = Paginator(qs, page_size)
- try:
- page_obj = paginator.page(page)
- except Exception:
- return Response({
- 'code': 0, 'msg': '获取成功',
- 'data': {'list': [], 'has_more': False, 'page': page, 'page_size': page_size}
- })
-
- records = list(page_obj)
- fadan_ids = [r.id for r in records]
-
- # 批量获取图片,按用途分组
- zhengju_map = {} # key: fadan_id, value: [url, ...]
- shensu_map = {}
- if fadan_ids:
- tupians = FadanShensuTupian.objects.filter(
- fadan_id__in=fadan_ids
- ).values('fadan_id', 'tupian_url', 'yongtu')
- for t in tupians:
- fid = t['fadan_id']
- url = t['tupian_url']
- if t['yongtu'] == 1:
- zhengju_map.setdefault(fid, []).append(url)
- else:
- shensu_map.setdefault(fid, []).append(url)
-
- # 组装返回数据
- chufa_list = []
- for r in records:
- rid = r.id
- item = {
- 'id': rid,
- 'beichufa_id': r.beichufa_id,
- 'guanliandingdan_id': r.guanliandingdan_id or '',
- 'chufaliyou': r.chufaliyou or '',
- 'fakuanjine': str(r.fakuanjine),
- 'zhuangtai': r.zhuangtai,
- 'yingxiang_qiangdan': r.yingxiang_qiangdan,
- 'shensuliyou': r.shensuliyou or '',
- 'bohuiliyou': r.bohuiliyou or '',
- 'zhengju_tupian': zhengju_map.get(rid, []), # 证据图片
- 'shensu_tupian': shensu_map.get(rid, []), # 申诉图片
- 'create_time': r.create_time.strftime('%Y-%m-%d %H:%M:%S') if r.create_time else '',
- 'update_time': r.update_time.strftime('%Y-%m-%d %H:%M:%S') if r.update_time else '',
- }
- chufa_list.append(item)
-
- has_more = page_obj.has_next()
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': {
- 'list': chufa_list,
- 'has_more': has_more,
- 'page': page,
- 'page_size': page_size,
- }
- })
-
- except Exception as e:
- logger.error(f"获取罚款列表异常 {yonghuid if 'yonghuid' in locals() else ''}: {e}", exc_info=True)
- return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
-
-
-class FaKuanShenSuView(APIView):
- """
- 罚款申诉提交/修改接口
- 路径:POST /yonghu/fkss
- 参数:
- fadan_id: 罚单ID
- shensu_liyou: 申诉理由
- shensu_tupian_urls: 图片相对URL数组(可空数组,但至少要有理由)
- 逻辑:
- - 仅待缴纳状态且无驳回理由的罚单可申诉
- - 允许对已有申诉进行修改:先删除旧申诉图片(yongtu=2),再添加新图片
- """
- permission_classes = [IsAuthenticated, DashouPermission]
-
- def post(self, request):
- try:
- user_main = request.user
- yonghuid = user_main.yonghuid
- fadan_id = request.data.get('fadan_id')
- shensu_liyou = request.data.get('shensu_liyou', '').strip()
- tupian_urls = request.data.get('shensu_tupian_urls', [])
-
- if not fadan_id:
- return Response({'code': 400, 'msg': '缺少罚款ID'}, status=400)
- if not shensu_liyou:
- return Response({'code': 400, 'msg': '请输入申诉理由'}, status=400)
-
- # 查询罚单,确保属于当前打手
- try:
- fadan = Fadan.objects.get(id=fadan_id, beichufa_id=yonghuid)
- except Fadan.DoesNotExist:
- return Response({'code': 404, 'msg': '罚单不存在或无权操作'}, status=404)
-
- # 只允许待缴纳状态(状态1)且没有驳回理由的罚单申诉
- if fadan.zhuangtai != 1:
- return Response({'code': 400, 'msg': '当前状态不可申诉'}, status=400)
- if fadan.bohuiliyou:
- return Response({'code': 400, 'msg': '该罚单申诉已被驳回,不可再次申诉'}, status=400)
-
- # 更新罚单状态为申诉中,并记录理由
- fadan.zhuangtai = 3
- fadan.shensuliyou = shensu_liyou
- fadan.save()
-
- # 删除旧的申诉图片(yongtu=2),仅删除打手自己上传的申诉图
- FadanShensuTupian.objects.filter(fadan=fadan, yongtu=2).delete()
-
- # 插入新上传的申诉图片
- for url in tupian_urls:
- FadanShensuTupian.objects.create(
- fadan=fadan,
- beichufa_id=yonghuid,
- tupian_url=url,
- yongtu=2
- )
-
- return Response({'code': 0, 'msg': '申诉提交成功'})
-
- except Exception as e:
- logger.error(f"罚款申诉异常 {yonghuid if 'yonghuid' in locals() else ''}: {e}", exc_info=True)
- return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
-
-
-
-
-
-
-
-# 需要替换为你自己项目中的导入路径
-from shangpin.models import Czjilu
-import string # 【致命错误修正】必须导入 string
-
-logger = logging.getLogger('xiaochengxu')
-
-# ==================== 1. 罚款支付下单接口 ====================
-
-class FaKuanPayView(APIView):
- """
- 罚款微信支付下单接口(完全照抄 YajinGoumai)
- 路径:POST /yonghu/fkjn
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- fadan_id = request.data.get('fadan_id')
- if not fadan_id:
- return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- fadan = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid)
- except Fadan.DoesNotExist:
- return Response({'code': 404, 'message': '罚单不存在或不属于您'}, status=status.HTTP_404_NOT_FOUND)
-
- if fadan.zhuangtai not in [1, 3]:
- return Response({'code': 400, 'message': '罚单当前状态不可支付'}, status=status.HTTP_400_BAD_REQUEST)
-
- jine = float(fadan.fakuanjine)
-
- timestamp_ms = int(time.time() * 1000)
- timestamp_ns = int(time.time_ns() // 1000) % 1000000
- random_num = random.randint(0, 99)
- dingdanid = f"FK{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
-
- chongzhi_jilu = Czjilu.objects.create(
- dingdan_id=dingdanid,
- zhuangtai=9,
- yonghuid=request.user.yonghuid,
- jine=jine,
- leixing=4,
- shuoming=settings.FAKUAN_PAY_DESCRIPTION
- )
-
- try:
- pay_params = self.generate_wechat_pay_params(
- dingdanid=dingdanid,
- jine=jine,
- openid=request.user.openid,
- pay_type='fakuan'
- )
- except Exception as e:
- chongzhi_jilu.delete()
- logger.error(f"生成微信支付参数失败: {str(e)}")
- return Response({
- 'code': 500,
- 'message': f'支付参数生成失败: {str(e)}'
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- return Response({
- 'code': 200,
- 'message': '支付参数生成成功',
- 'payParams': pay_params,
- 'dingdanid': dingdanid
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f"罚款支付下单接口异常: {str(e)}", exc_info=True)
- return Response({
- 'code': 500,
- 'message': '服务器内部错误,请稍后重试'
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
- APPID = settings.WEIXIN_APPID
- MCHID = settings.WEIXIN_MCHID
- KEY = settings.WEIXIN_SHANGHUMIYAO
-
- if pay_type == 'fakuan':
- PAY_DESCRIPTION = settings.FAKUAN_PAY_DESCRIPTION
- NOTIFY_URL = settings.FAKUAN_PAY_NOTIFY_URL # 回调地址在这里
- else:
- raise Exception('未知的支付类型')
-
- nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
- total_fee = int(jine * 100)
- trade_type = 'JSAPI'
- user_ip = self.get_client_ip()
-
- # 第一次签名
- sign_data = {
- 'appid': APPID,
- 'mch_id': MCHID,
- 'nonce_str': nonce_str,
- 'body': PAY_DESCRIPTION,
- 'out_trade_no': dingdanid,
- 'total_fee': total_fee,
- 'spbill_create_ip': user_ip,
- 'notify_url': NOTIFY_URL, # 关键:把回调地址传入
- 'trade_type': trade_type,
- 'openid': openid
- }
-
- sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
- sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
- sign_string += f'&key={KEY}'
- sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
-
- # 构建 XML,这里必须包含 notify_url
- xml_data = f"""
-{APPID}
-{PAY_DESCRIPTION}
-{MCHID}
-{nonce_str}
-{NOTIFY_URL}
-{openid}
-{dingdanid}
-{user_ip}
-{total_fee}
-{trade_type}
-{sign}
-"""
-
- response = requests.post(
- 'https://api.mch.weixin.qq.com/pay/unifiedorder',
- data=xml_data.encode('utf-8'),
- headers={'Content-Type': 'application/xml'},
- timeout=10
- )
-
- root = ET.fromstring(response.content)
- return_code = root.find('return_code').text
- result_code = root.find('result_code').text
-
- if return_code == 'SUCCESS' and result_code == 'SUCCESS':
- prepay_id = root.find('prepay_id').text
- timestamp = str(int(time.time()))
- nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
- package = f'prepay_id={prepay_id}'
- sign_type = 'MD5'
-
- pay_sign_data = {
- 'appId': APPID,
- 'timeStamp': timestamp,
- 'nonceStr': nonce_str,
- 'package': package,
- 'signType': sign_type
- }
-
- sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
- pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
- pay_sign_string += f'&key={KEY}'
- pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
-
- return {
- 'appId': APPID,
- 'timeStamp': timestamp,
- 'nonceStr': nonce_str,
- 'package': package,
- 'signType': sign_type,
- 'paySign': pay_sign
- }
- else:
- err_code = root.find('err_code')
- err_code_des = root.find('err_code_des')
- error_details = f"return_code: {return_code}"
- if err_code is not None:
- error_details += f", err_code: {err_code.text}"
- if err_code_des is not None:
- error_details += f", err_code_des: {err_code_des.text}"
- raise Exception(f"微信支付下单失败: {error_details}")
-
- def get_client_ip(self):
- request = self.request
- x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
- if x_forwarded_for:
- ip = x_forwarded_for.split(',')[0]
- else:
- ip = request.META.get('REMOTE_ADDR')
- return ip if ip else '127.0.0.1'
-
-
-# ==================== 2. 罚款支付回调接口 ====================
-from django.views import View
-class FaKuanHuitiaoView(View):
-
- """
- 罚款支付回调接口(严格借鉴 YajinHuitiao 接口,参数一个不少)
- 路径:POST /yonghu/fk_huitiao
- """
- def post(self, request):
- try:
- # 1. 解析微信回调XML数据
- request_body = request.body
- logger.info(f"收到微信支付回调,原始数据: {request_body.decode('utf-8')}")
-
- root = ET.fromstring(request_body)
-
- # 获取关键字段
- return_code = root.find('return_code').text
- result_code = root.find('result_code').text
- out_trade_no = root.find('out_trade_no').text # 订单ID
- transaction_id = root.find('transaction_id').text # 微信交易号
- total_fee = int(root.find('total_fee').text) # 支付金额(分)
-
- logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分")
-
- # 2. 验证基本返回状态
- if return_code != 'SUCCESS' or result_code != 'SUCCESS':
- logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}")
- return self.wechat_response('FAIL', '支付失败')
-
- # 3. 验证签名(确保是微信发来的)
- if not self.verify_signature(request_body):
- logger.error(f"签名验证失败: {out_trade_no}")
- return self.wechat_response('FAIL', '签名验证失败')
-
- # 4. 查询订单
- try:
- order = Czjilu.objects.get(dingdan_id=out_trade_no)
- logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}")
- except Czjilu.DoesNotExist:
- logger.error(f"订单不存在: {out_trade_no}")
- return self.wechat_response('FAIL', '订单不存在')
-
- # 5. 检查订单状态是否为未支付(9)
- if order.zhuangtai != 9:
- logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
- # 已处理过的订单直接返回成功,避免微信重复回调
- return self.wechat_response('SUCCESS', 'OK')
-
- # 6. 验证金额(微信返回的是分,需要转换)
- wechat_amount_yuan = total_fee / 100 # 转换为元
- order_amount_yuan = float(order.jine) # 订单金额(元)
-
- # 允许1分钱的误差(浮点数比较)
- if abs(wechat_amount_yuan - order_amount_yuan) > 0.01:
- logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
- return self.wechat_response('FAIL', '金额不一致')
-
- # 7. 更新订单状态为已支付(3)
- order.zhuangtai = 3
- order.save(update_fields=['zhuangtai'])
- logger.info(f"订单状态更新成功: {order.dingdan_id} -> 3")
-
- # 8. 更新罚单状态为已缴纳(2)
- try:
- # 根据订单中的用户ID和金额匹配待缴纳的罚单
- fadan = Fadan.objects.filter(
- beichufa_id=order.yonghuid,
- fakuanjine=order.jine,
- zhuangtai__in=[1, 3]
- ).first()
- if fadan:
- fadan.zhuangtai = 2 # 已缴纳
- fadan.save(update_fields=['zhuangtai'])
- logger.info(f"罚单 {fadan.id} 已自动标记为已缴纳")
-
- from yonghu.fadan_fenhong_utils import process_fadan_fenhong
- success, msg = process_fadan_fenhong(fadan.id)
- if not success:
- logger.error(f"罚单 {fadan.id} 分红处理失败: {msg}")
- else:
- logger.info(f"罚单 {fadan.id} 分红处理完成: {msg}")
-
- except Exception as e:
- logger.error(f"更新罚单状态失败: {str(e)}", exc_info=True)
-
- # 9. 更新收支记录表
- try:
- from peizhi.models import Szjilu
- from decimal import Decimal
-
- # 获取订单金额(确保是Decimal类型)
- jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
-
- from dingdan.utils import update_daily_income
- update_daily_income(jine_decimal)
-
- # 查询或创建ID=1的收支记录
- szjilu, created = Szjilu.objects.get_or_create(
- id=1,
- defaults={
- 'zongsy': Decimal('0.00'),
- 'zongls': Decimal('0.00'),
- 'zongzc': Decimal('0.00'),
- 'jrzc': Decimal('0.00'),
- 'jrls': Decimal('0.00')
- }
- )
-
- # 更新三个字段
- szjilu.zongsy += jine_decimal # 总收益
- szjilu.zongls += jine_decimal # 总流水
- szjilu.jrls += jine_decimal # 今日流水
-
- szjilu.save()
-
- logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元")
-
- except Exception as e:
- # 收支记录更新失败不影响主流程,只记录日志
- logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
-
- # 10. 返回成功响应给微信
- logger.info(f"罚款支付回调处理完成: {out_trade_no}")
- return self.wechat_response('SUCCESS', 'OK')
-
- except ET.ParseError as e:
- logger.error(f"XML解析失败: {str(e)}")
- return self.wechat_response('FAIL', '数据格式错误')
- except Exception as e:
- logger.error(f"罚款支付回调处理异常: {str(e)}", exc_info=True)
- # 返回FAIL让微信重试
- return self.wechat_response('FAIL', '处理异常')
-
- def verify_signature(self, xml_data):
- """验证微信支付签名(完全照搬 YajinHuitiao 的 verify_signature)"""
- try:
- root = ET.fromstring(xml_data)
- sign = root.find('sign').text
-
- if not sign:
- logger.error("签名字段不存在")
- return False
-
- # 获取所有参数并排除sign
- params = {}
- for child in root:
- if child.tag != 'sign' and child.text is not None:
- params[child.tag] = child.text
-
- # 按照参数名ASCII码从小到大排序
- sorted_params = sorted(params.items(), key=lambda x: x[0])
-
- # 拼接字符串
- sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
- sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
-
- # 计算签名
- calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
-
- is_valid = sign == calculated_sign
- if not is_valid:
- logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}")
-
- return is_valid
-
- except Exception as e:
- logger.error(f"验证签名异常: {str(e)}")
- return False
-
- def wechat_response(self, return_code, return_msg):
- """返回微信支付回调响应(完全照搬 YajinHuitiao 的 wechat_response)"""
- xml_response = f"""
-
-
-"""
-
- logger.info(f"返回微信响应: {return_code} - {return_msg}")
- return HttpResponse(xml_response, content_type='application/xml')
-
-
-# ==================== 3. 支付轮询接口 ====================
-class FaKuanPayPollView(APIView):
- """罚款支付轮询接口(借鉴 YajinLunxun)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- dingdanid = request.data.get('dingdanid')
- if not dingdanid:
- return Response({'code': 400, 'message': '订单ID不能为空'})
-
- order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid)
- if order.zhuangtai == 3:
- return Response({'code': 200, 'message': '支付成功'})
- else:
- return Response({'code': 400, 'message': '订单尚未支付'})
- except Czjilu.DoesNotExist:
- return Response({'code': 400, 'message': '订单不存在'})
- except Exception as e:
- logger.error(f"轮询异常: {str(e)}")
- return Response({'code': 500, 'message': '服务器内部错误'})
-
-
-# ==================== 4. 支付失败处理接口 ====================
-class FaKuanPayFailView(APIView):
- """罚款支付失败处理接口(借鉴 YajinShibai)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- dingdanid = request.data.get('dingdanid')
- if not dingdanid:
- return Response({'code': 400, 'message': '订单ID不能为空'})
-
- order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid)
- if order.zhuangtai == 9: # 未支付
- order.delete()
- logger.info(f"用户{request.user.yonghuid}删除未支付订单: {dingdanid}")
- return Response({'code': 200, 'message': '订单已删除'})
- else:
- return Response({'code': 400, 'message': '订单状态不是未支付,无法删除'})
- except Czjilu.DoesNotExist:
- return Response({'code': 400, 'message': '订单不存在'})
- except Exception as e:
- logger.error(f"支付失败处理异常: {str(e)}")
- return Response({'code': 500, 'message': '服务器内部错误'})
-
-
-
-
-
-from shangpin.models import Czjilu
-from dengji.models import Chenghao, ShenheJilu, KaoheCishuFeiyong, YonghuChenghao
-from dengji.utils import get_tag_fee # 您需要确保此函数已定义
-
-from dengji.utils import create_shenhe_jilu
-from dengji.utils import validate_shenheguan
-
-logger = logging.getLogger('xiaochengxu')
-
-
-
-class KaohePayView(APIView):
- """
- 考核支付下单接口
- 路径:POST /yonghu/khzf
- 参数:
- tag_id: 标签ID
- game_nick: 游戏昵称
- remark: 备注
- reapply: 是否再次申请 (true/false)
- jilu_id: 再次申请时的原记录ID(reapply=true时必传)
- shenheguan_id: 指定考核官ID(可选)
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- user = request.user
- tag_id = request.data.get('tag_id')
- game_nick = request.data.get('game_nick', '').strip()
- remark = request.data.get('remark', '').strip()
- reapply = request.data.get('reapply', False)
- jilu_id = request.data.get('jilu_id') if reapply else None
- shenheguan_id = request.data.get('shenheguan_id', '').strip()
-
- logger.info(f"用户 {user.yonghuid} 发起考核支付申请,标签ID: {tag_id}, 再次申请: {reapply}")
-
- if not tag_id or not game_nick:
- logger.warning(f"用户 {user.yonghuid} 参数不完整: tag_id={tag_id}, game_nick={game_nick}")
- return Response({'code': 400, 'message': '标签ID和游戏昵称不能为空'})
-
- # 1. 检查是否有未支付的考核订单
- """unpaid_order = Czjilu.objects.filter(
- yonghuid=user.yonghuid,
- leixing=5,
- zhuangtai=9
- ).exists()
- if unpaid_order:
- logger.warning(f"用户 {user.yonghuid} 存在未支付的考核订单")
- return Response({'code': 400, 'message': '您有未支付的考核订单,请先完成支付或取消'})"""
-
- # 2. 验证标签
- try:
- chenghao = Chenghao.objects.get(id=tag_id)
- logger.info(f"标签验证通过: {chenghao.mingcheng}")
- except Chenghao.DoesNotExist:
- logger.error(f"标签不存在: tag_id={tag_id}")
- return Response({'code': 404, 'message': '标签不存在'})
-
- # 3. 检查用户是否已拥有该标签
- if YonghuChenghao.objects.filter(yonghu=user, chenghao=chenghao).exists():
- logger.warning(f"用户 {user.yonghuid} 已拥有标签 {chenghao.mingcheng}")
- return Response({'code': 400, 'message': '您已拥有该标签,无需重复考核'})
-
- # 4. 检查是否有未完成的申请
- if ShenheJilu.objects.filter(
- shenqingren_id=user.yonghuid,
- chenghao=chenghao,
- zhuangtai__in=[0, 3] # 审核中、待审核
- ).exists():
- logger.warning(f"用户 {user.yonghuid} 有未完成的考核申请")
- return Response({'code': 400, 'message': '您有未完成的考核申请,请等待结果'})
-
- # 5. 计算考核次数和费用
- if reapply and jilu_id:
- last = ShenheJilu.objects.filter(
- jilu_id=jilu_id,
- shenqingren_id=user.yonghuid,
- zhuangtai=2
- ).first()
- if not last:
- logger.warning(f"用户 {user.yonghuid} 再次申请失败:原记录不存在或状态不是未通过,jilu_id={jilu_id}")
- return Response({'code': 400, 'message': '原记录不存在或状态不是未通过'})
- new_cishu = last.cishu + 1
- logger.info(f"再次申请,原记录ID: {jilu_id},新次数: {new_cishu}")
- else:
- max_cishu = ShenheJilu.objects.filter(
- shenqingren_id=user.yonghuid,
- chenghao=chenghao,
- zhuangtai__in=[1, 2]
- ).aggregate(max=models.Max('cishu'))['max'] or 0
- new_cishu = max_cishu + 1
- logger.info(f"首次申请,计算次数: {new_cishu}")
-
- # 6. 获取本次费用
- fee = get_tag_fee(chenghao, new_cishu)
- logger.info(f"本次考核费用: {fee}元,次数: {new_cishu}")
-
- if fee <= 0:
- logger.warning(f"费用为0,应使用免费申请接口,tag_id={tag_id}, cishu={new_cishu}")
- return Response({'code': 400, 'message': '本次费用为0,请直接使用免费申请接口'})
-
- # 7. 生成订单ID
- timestamp_ms = int(time.time() * 1000)
- timestamp_ns = int(time.time_ns() // 1000) % 1000000
- random_num = random.randint(0, 99)
- dingdanid = f"KH{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
- logger.info(f"生成订单ID: {dingdanid}")
-
- # 8. 创建支付订单(Czjilu)
- order = Czjilu.objects.create(
- dingdan_id=dingdanid,
- zhuangtai=9, # 未支付
- yonghuid=user.yonghuid,
- jine=float(fee),
- leixing=5, # 5-考核支付
- shuoming=settings.KAOHE_PAY_DESCRIPTION
- )
- logger.info(f"创建支付订单成功: {dingdanid}")
-
- # 9. 保存考核支付临时上下文(用于回调时取业务参数)
- try:
- from dengji.models import KaohePayTemp
- KaohePayTemp.objects.create(
- dingdan_id=dingdanid,
- yonghuid=user.yonghuid,
- tag_id=tag_id,
- game_nick=game_nick,
- remark=remark,
- reapply=reapply,
- jilu_id=jilu_id if reapply else None,
- shenheguan_id=shenheguan_id,
- cishu=new_cishu,
- fee=fee,
- )
- logger.info(f"保存临时上下文成功: {dingdanid}")
- except Exception as e:
- logger.error(f"保存临时上下文失败: {str(e)}", exc_info=True)
- # 临时表保存失败应该回滚订单
- order.delete()
- return Response({'code': 500, 'message': '系统异常,请稍后重试'})
-
- # 10. 构造 attach 参数(只传订单ID,确保不超127字节)
- attach_str = json.dumps({'oid': dingdanid}, ensure_ascii=False)
- logger.info(f"attach内容: {attach_str},长度: {len(attach_str.encode('utf-8'))}字节")
-
- # 11. 生成微信支付参数
- try:
- pay_params = self.generate_wechat_pay_params(
- dingdanid=dingdanid,
- jine=float(fee),
- openid=user.openid,
- attach=attach_str,
- pay_type='kaohe'
- )
- logger.info(f"微信支付参数生成成功: {dingdanid}")
- except Exception as e:
- order.delete() # 下单失败,删除订单记录
- # 同时删除临时表记录
- try:
- from dengji.models import KaohePayTemp
- KaohePayTemp.objects.filter(dingdan_id=dingdanid).delete()
- except Exception as del_e:
- logger.error(f"删除临时表记录失败: {str(del_e)}")
- logger.error(f"生成微信支付参数失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'message': f'支付参数生成失败: {str(e)}'})
-
- return Response({
- 'code': 200,
- 'message': '支付参数生成成功',
- 'payParams': pay_params,
- 'dingdanid': dingdanid
- })
-
- except Exception as e:
- logger.error(f"考核支付下单异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'message': '服务器内部错误'})
-
- def generate_wechat_pay_params(self, dingdanid, jine, openid, attach, pay_type):
- """生成微信JSAPI支付参数"""
- APPID = settings.WEIXIN_APPID
- MCHID = settings.WEIXIN_MCHID
- KEY = settings.WEIXIN_SHANGHUMIYAO
-
- if pay_type == 'kaohe':
- PAY_DESCRIPTION = settings.KAOHE_PAY_DESCRIPTION
- NOTIFY_URL = settings.KAOHE_PAY_NOTIFY_URL
- else:
- raise Exception('未知支付类型')
-
- nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
- total_fee = int(jine * 100)
- trade_type = 'JSAPI'
- user_ip = self.get_client_ip()
-
- logger.info(f"微信支付下单参数: out_trade_no={dingdanid}, total_fee={total_fee}, openid={openid}, ip={user_ip}")
-
- # 第一次签名(统一下单)
- sign_data = {
- 'appid': APPID,
- 'mch_id': MCHID,
- 'nonce_str': nonce_str,
- 'body': PAY_DESCRIPTION,
- 'out_trade_no': dingdanid,
- 'total_fee': total_fee,
- 'spbill_create_ip': user_ip,
- 'notify_url': NOTIFY_URL,
- 'trade_type': trade_type,
- 'openid': openid,
- 'attach': attach,
- }
-
- sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
- sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
- sign_string += f'&key={KEY}'
- sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
-
- # 构建XML请求体
- xml_data = f"""
-{APPID}
-{PAY_DESCRIPTION}
-{MCHID}
-{nonce_str}
-{NOTIFY_URL}
-{openid}
-{dingdanid}
-{user_ip}
-{total_fee}
-{trade_type}
-
-{sign}
-"""
-
- logger.info(f"微信支付请求XML: {xml_data[:500]}...") # 只打印前500字符
-
- response = requests.post(
- 'https://api.mch.weixin.qq.com/pay/unifiedorder',
- data=xml_data.encode('utf-8'),
- headers={'Content-Type': 'application/xml'},
- timeout=10
- )
-
- logger.info(f"微信支付响应: {response.text}")
-
- root = ET.fromstring(response.content)
-
- # 安全获取return_code
- return_code_node = root.find('return_code')
- if return_code_node is None or return_code_node.text is None:
- logger.error(f"微信返回缺少return_code节点,原始内容: {response.text}")
- raise Exception('微信支付接口异常,缺少return_code')
- return_code = return_code_node.text
-
- # 安全获取result_code
- result_code_node = root.find('result_code')
- if result_code_node is None or result_code_node.text is None:
- logger.error(f"微信返回缺少result_code节点,原始内容: {response.text}")
- raise Exception('微信支付接口异常,缺少result_code')
- result_code = result_code_node.text
-
- if return_code == 'SUCCESS' and result_code == 'SUCCESS':
- prepay_id_node = root.find('prepay_id')
- if prepay_id_node is None or prepay_id_node.text is None:
- logger.error(f"统一下单成功但未返回prepay_id,原始内容: {response.text}")
- raise Exception('微信支付接口异常,缺少prepay_id')
- prepay_id = prepay_id_node.text
-
- timestamp = str(int(time.time()))
- nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
- package = f'prepay_id={prepay_id}'
- sign_type = 'MD5'
-
- # 第二次签名(前端调起支付用)
- pay_sign_data = {
- 'appId': APPID,
- 'timeStamp': timestamp,
- 'nonceStr': nonce_str,
- 'package': package,
- 'signType': sign_type,
- }
- sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
- pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
- pay_sign_string += f'&key={KEY}'
- pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
-
- logger.info(f"微信支付预下单成功,prepay_id={prepay_id}")
-
- return {
- 'appId': APPID,
- 'timeStamp': timestamp,
- 'nonceStr': nonce_str,
- 'package': package,
- 'signType': sign_type,
- 'paySign': pay_sign,
- }
- else:
- # 安全获取错误信息
- err_code_node = root.find('err_code')
- err_code = err_code_node.text if err_code_node is not None else '未知错误码'
- err_code_des_node = root.find('err_code_des')
- err_code_des = err_code_des_node.text if err_code_des_node is not None else '未知错误描述'
- error_details = f"return_code: {return_code}, result_code: {result_code}, err_code: {err_code}, err_code_des: {err_code_des}"
- logger.error(f"微信支付下单失败: {error_details}")
- raise Exception(f"微信支付下单失败: {error_details}")
-
- def get_client_ip(self):
- """获取客户端IP"""
- request = self.request
- x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
- if x_forwarded_for:
- ip = x_forwarded_for.split(',')[0]
- else:
- ip = request.META.get('REMOTE_ADDR', '127.0.0.1')
- return ip if ip else '127.0.0.1'
-
-
-class KaohePayCallbackView(View):
- """
- 考核支付回调接口
- 路径:/yonghu/kh_huitiao
- """
-
- def post(self, request):
- try:
- request_body = request.body
- logger.info(f"收到考核支付回调: {request_body.decode('utf-8')}")
-
- root = ET.fromstring(request_body)
-
- # 安全获取return_code
- return_code_node = root.find('return_code')
- if return_code_node is None or return_code_node.text is None:
- logger.error(f"微信回调XML缺少return_code节点,原始内容: {request_body.decode('utf-8')}")
- return self.wechat_response('FAIL', '数据格式错误')
- return_code = return_code_node.text
-
- # 安全获取result_code
- result_code_node = root.find('result_code')
- if result_code_node is None or result_code_node.text is None:
- logger.error(f"微信回调XML缺少result_code节点,原始内容: {request_body.decode('utf-8')}")
- return self.wechat_response('FAIL', '数据格式错误')
- result_code = result_code_node.text
-
- # 安全获取out_trade_no
- out_trade_no_node = root.find('out_trade_no')
- if out_trade_no_node is None or out_trade_no_node.text is None:
- logger.error(f"微信回调XML缺少out_trade_no节点,原始内容: {request_body.decode('utf-8')}")
- return self.wechat_response('FAIL', '数据格式错误')
- out_trade_no = out_trade_no_node.text
-
- # 安全获取transaction_id
- transaction_id_node = root.find('transaction_id')
- if transaction_id_node is None or transaction_id_node.text is None:
- logger.error(f"微信回调XML缺少transaction_id节点,原始内容: {request_body.decode('utf-8')}")
- return self.wechat_response('FAIL', '数据格式错误')
- transaction_id = transaction_id_node.text
-
- # 安全获取total_fee
- total_fee_node = root.find('total_fee')
- if total_fee_node is None or total_fee_node.text is None:
- logger.error(f"微信回调XML缺少total_fee节点,原始内容: {request_body.decode('utf-8')}")
- return self.wechat_response('FAIL', '数据格式错误')
- total_fee = int(total_fee_node.text)
-
- logger.info(
- f"回调参数解析成功: out_trade_no={out_trade_no}, transaction_id={transaction_id}, total_fee={total_fee}")
-
- if return_code != 'SUCCESS' or result_code != 'SUCCESS':
- logger.warning(f"支付状态异常: return_code={return_code}, result_code={result_code}")
- return self.wechat_response('FAIL', '支付失败')
-
- # 验证签名
- if not self.verify_signature(request_body):
- logger.error(f"签名验证失败,订单号: {out_trade_no}")
- return self.wechat_response('FAIL', '签名验证失败')
- logger.info(f"签名验证成功,订单号: {out_trade_no}")
-
- # 查找订单
- try:
- order = Czjilu.objects.get(dingdan_id=out_trade_no, leixing=5)
- logger.info(f"找到订单: {out_trade_no}, 当前状态: {order.zhuangtai}")
- except Czjilu.DoesNotExist:
- logger.error(f"考核订单不存在: {out_trade_no}")
- return self.wechat_response('FAIL', '订单不存在')
-
- if order.zhuangtai != 9:
- logger.warning(f"订单状态非未支付: {order.zhuangtai}, 订单号: {out_trade_no}")
- # 已支付的订单直接返回成功,避免微信重复回调
- if order.zhuangtai == 3:
- logger.info(f"订单已支付,直接返回成功: {out_trade_no}")
- return self.wechat_response('SUCCESS', 'OK')
- return self.wechat_response('FAIL', '订单状态异常')
-
- # 验证金额
- wechat_yuan = total_fee / 100
- if abs(wechat_yuan - float(order.jine)) > 0.01:
- logger.error(f"金额不一致: 订单金额={order.jine}, 回调金额={wechat_yuan}, 订单号: {out_trade_no}")
- return self.wechat_response('FAIL', '金额不一致')
- logger.info(f"金额验证通过: {wechat_yuan}元")
-
- # 更新订单状态为已支付
- order.zhuangtai = 3
- order.save()
- logger.info(f"订单状态更新为已支付: {out_trade_no}")
-
- # 从临时表获取业务参数并创建审核记录
- try:
- from dengji.models import KaohePayTemp
- from dengji.utils import create_shenhe_jilu_from_temp
- from yonghu.models import UserMain
-
- temp = KaohePayTemp.objects.get(dingdan_id=out_trade_no, yonghuid=order.yonghuid)
- logger.info(f"找到临时表记录: {out_trade_no}, 用户ID: {order.yonghuid}")
-
- user = UserMain.objects.get(yonghuid=order.yonghuid)
- logger.info(f"获取用户信息成功: {user.yonghuid}")
-
- success, msg, record = create_shenhe_jilu_from_temp(user, temp)
- if success:
- logger.info(f"创建审核记录成功: jilu_id={record.jilu_id}, 订单号: {out_trade_no}")
- else:
- logger.error(f"创建审核记录失败: {msg}, 订单号: {out_trade_no}")
- # 订单已支付但记录创建失败,需要标记订单异常,人工介入
- order.zhuangtai = 10 # 10表示支付成功但业务处理失败
- order.save()
- logger.error(f"订单状态标记为异常(10): {out_trade_no}")
-
- except KaohePayTemp.DoesNotExist:
- logger.error(f"临时表无记录,订单号: {out_trade_no}, 用户ID: {order.yonghuid}")
- # 尝试从attach解析(兼容旧数据)
- attach_element = root.find('attach')
- if attach_element is not None and attach_element.text:
- try:
- attach_data = json.loads(attach_element.text)
- logger.info(f"从attach解析数据成功: {attach_data}")
- # 这里可以保留旧逻辑,但建议只记录日志
- except Exception as e:
- logger.error(f"解析attach失败: {str(e)}")
- except Exception as e:
- logger.error(f"处理临时表或创建审核记录异常: {str(e)}", exc_info=True)
- # 标记订单异常
- order.zhuangtai = 10
- order.save()
- logger.error(f"订单状态标记为异常(10): {out_trade_no}")
-
- # 更新收支记录
- try:
- from peizhi.models import Szjilu
- from decimal import Decimal
- from dingdan.utils import update_daily_income
-
- jine_decimal = Decimal(str(order.jine))
- update_daily_income(jine_decimal)
- logger.info(f"更新每日收入成功: {jine_decimal}元")
-
- szjilu, created = Szjilu.objects.get_or_create(
- id=1,
- defaults={
- 'zongsy': Decimal('0.00'),
- 'zongls': Decimal('0.00'),
- 'zongzc': Decimal('0.00'),
- 'jrzc': Decimal('0.00'),
- 'jrls': Decimal('0.00'),
- }
- )
- szjilu.zongsy += jine_decimal
- szjilu.zongls += jine_decimal
- szjilu.jrls += jine_decimal
- szjilu.save()
- logger.info(f"更新收支记录成功: zongsy={szjilu.zongsy}")
- except Exception as e:
- logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
-
- logger.info(f"考核支付回调处理完成,订单号: {out_trade_no}")
- return self.wechat_response('SUCCESS', 'OK')
-
- except ET.ParseError as e:
- logger.error(f"XML解析失败: {str(e)}, 原始数据: {request.body.decode('utf-8')}")
- return self.wechat_response('FAIL', '数据格式错误')
- except Exception as e:
- logger.error(f"考核支付回调异常: {str(e)}", exc_info=True)
- return self.wechat_response('FAIL', '处理异常')
-
- def verify_signature(self, xml_data):
- """验证微信支付签名"""
- try:
- root = ET.fromstring(xml_data)
- sign_node = root.find('sign')
- if sign_node is None or sign_node.text is None:
- logger.error("微信回调XML缺少sign节点")
- return False
- sign = sign_node.text
-
- params = {}
- for child in root:
- if child.tag != 'sign' and child.text is not None:
- params[child.tag] = child.text
-
- sorted_params = sorted(params.items(), key=lambda x: x[0])
- sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
- sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
- calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
-
- result = sign == calculated_sign
- if not result:
- logger.error(f"签名验证失败: 原签名={sign}, 计算签名={calculated_sign}")
- return result
- except Exception as e:
- logger.error(f"签名验证异常: {str(e)}", exc_info=True)
- return False
-
- def wechat_response(self, return_code, return_msg):
- """构造微信回调响应XML"""
- xml = f"""
-
-
-"""
- logger.info(f"返回微信响应: {xml}")
- return HttpResponse(xml, content_type='application/xml')
-
-
-
-
-
-
-
-class KaohePayPollView(APIView):
- """考核支付轮询"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- dingdanid = request.data.get('dingdanid')
- if not dingdanid:
- return Response({'code': 400, 'message': '订单ID不能为空'})
- try:
- order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5)
- if order.zhuangtai == 3:
- return Response({'code': 200, 'message': '支付成功'})
- return Response({'code': 400, 'message': '订单尚未支付'})
- except Czjilu.DoesNotExist:
- return Response({'code': 400, 'message': '订单不存在'})
-
-
-class KaohePayFailView(APIView):
- """考核支付失败处理"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- dingdanid = request.data.get('dingdanid')
- if not dingdanid:
- return Response({'code': 400, 'message': '订单ID不能为空'})
- try:
- order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5)
- if order.zhuangtai == 9:
- order.delete()
- return Response({'code': 200, 'message': '订单已删除'})
- return Response({'code': 400, 'message': '订单状态无法删除'})
- except Czjilu.DoesNotExist:
- return Response({'code': 400, 'message': '订单不存在'})
-
-
-
-
-
-
-
-
-class TixianAssetView(APIView):
- """
- 获取用户所有可提现资产及费率
- 请求方式:POST
- 认证:JWT Token
- 返回:
- {
- "code": 200,
- "msg": "success",
- "data": {
- "dashou_yue": "123.45",
- "dashou_yajin": "500.00",
- "shangjia_yue": "0.00",
- "guanshi_fenyong": "67.89",
- "zuzhang_fenyong": "0.00",
- "kaoheguan_fenyong": "0.00",
- "dashou_rate": 0.08,
- "dashou_yajin_rate": 0.05,
- "shangjia_rate": 0.06,
- "guanshi_rate": 0.08,
- "zuzhang_rate": 0.05,
- "kaoheguan_rate": 0.07
- }
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- user = request.user
- # 初始化返回数据,默认全部为0
- data = {
- 'dashou_yue': '0.00',
- 'dashou_yajin': '0.00',
- 'shangjia_yue': '0.00',
- 'guanshi_fenyong': '0.00',
- 'zuzhang_fenyong': '0.00',
- 'kaoheguan_fenyong': '0.00',
- 'dashou_rate': 0.00,
- 'dashou_yajin_rate': 0.00,
- 'shangjia_rate': 0.00,
- 'guanshi_rate': 0.00,
- 'zuzhang_rate': 0.00,
- 'kaoheguan_rate': 0.00,
- }
-
- # ------------------- 打手资产 -------------------
- try:
- dashou = user.dashou_profile
- data['dashou_yue'] = str(dashou.yue)
- data['dashou_yajin'] = str(dashou.yajin)
- except ObjectDoesNotExist:
- pass
-
- # ------------------- 商家资产 -------------------
- try:
- shangjia = user.shop_profile
- data['shangjia_yue'] = str(shangjia.yue)
- except ObjectDoesNotExist:
- pass
-
- # ------------------- 管事资产 -------------------
- try:
- guanshi = user.guanshi_profile
- data['guanshi_fenyong'] = str(guanshi.yue)
- except ObjectDoesNotExist:
- pass
-
- # ------------------- 组长资产 -------------------
- try:
- zuzhang = user.zuzhang_profile
- data['zuzhang_fenyong'] = str(zuzhang.ketixian_jine)
- except ObjectDoesNotExist:
- pass
-
- # ------------------- 审核官(考核官)资产 -------------------
- try:
- kaoheguan = user.shenheguan_profile
- data['kaoheguan_fenyong'] = str(kaoheguan.yue)
- except ObjectDoesNotExist:
- pass
-
- # ------------------- 费率查询(从 Lilubiao 获取) -------------------
- # 打手提现费率 (类型 '5')
- lilu_obj = Lilubiao.objects.filter(fadanpingtai='5').first()
- data['dashou_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
-
- # 打手保证金提现费率 (类型 '11')
- lilu_obj = Lilubiao.objects.filter(fadanpingtai='11').first()
- data['dashou_yajin_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
-
- # 商家提现费率 (类型 '10')
- lilu_obj = Lilubiao.objects.filter(fadanpingtai='10').first()
- data['shangjia_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
-
- # 管事提现费率 (类型 '6')
- lilu_obj = Lilubiao.objects.filter(fadanpingtai='6').first()
- data['guanshi_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
-
- # 组长提现费率 (类型 '8')
- lilu_obj = Lilubiao.objects.filter(fadanpingtai='8').first()
- data['zuzhang_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
-
- # 审核官(考核官)提现费率 (类型 '9')
- lilu_obj = Lilubiao.objects.filter(fadanpingtai='9').first()
- data['kaoheguan_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
-
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': data
- }, status=status.HTTP_200_OK)
-
-
-
-
-
-class GuanshiContactView(APIView):
- """
- 管事获取自己的邀请人(组长)信息
- 路径: /api/yonghu/guanshi_contact
- 方法: POST
- 权限: JWT认证
- 请求体: 无(仅需携带合法token)
- 成功返回:
- code: 200,
- data: {
- uid: "1234567", // 组长用户ID
- nicheng: "张三", // 组长昵称
- touxiang: "avatar/xxx.jpg" // 组长头像相对路径(前端拼接完整URL)
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 获取当前登录用户(必须已通过JWT认证)
- current_user = request.user # 类型: UserMain
-
- # 2. 校验是否为管事身份(存在管事扩展表记录)
- try:
- guanshi_profile = current_user.guanshi_profile
- except ObjectDoesNotExist:
- return Response({
- 'code': 403,
- 'msg': '您还不是管事,无法获取组长信息',
- 'data': None
- })
-
- # 3. 获取邀请人ID(管事扩展表中的 yaoqingren 字段存储的是邀请人的 yonghuid)
- inviter_yonghuid = guanshi_profile.yaoqingren
- if not inviter_yonghuid:
- return Response({
- 'code': 404,
- 'msg': '未找到邀请人信息',
- 'data': None
- })
-
- # 4. 根据邀请人用户ID查询用户主表
- try:
- inviter_user = UserMain.objects.get(yonghuid=inviter_yonghuid)
- except UserMain.DoesNotExist:
- logger.error(f"邀请人用户不存在: yonghuid={inviter_yonghuid}")
- return Response({
- 'code': 404,
- 'msg': '邀请人账号异常',
- 'data': None
- })
-
- # 5. 获取邀请人昵称(从老板扩展表取,若不存在则给默认值)
- try:
- inviter_nicheng = inviter_user.boss_profile.nickname
- except ObjectDoesNotExist:
- inviter_nicheng = '组长' # 兜底昵称
-
- # 6. 构造返回数据
- response_data = {
- 'uid': inviter_user.yonghuid,
- 'nicheng': inviter_nicheng,
- 'touxiang': inviter_user.avatar or '' # 头像相对路径
- }
-
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': response_data
- })
-
-
-
-
-
-
-
-
-
-import logging
-
-import requests
-from django.db import transaction
-from django.conf import settings
-from django.core.cache import cache
-from django.utils import timezone
-from rest_framework.views import APIView
-from rest_framework import permissions, status
-from rest_framework.response import Response
-
-
-
-
-
-
-logger = logging.getLogger(__name__)
-
-def generate_tixian_id():
- """生成唯一提现订单号:TX + 时间戳(13位) + 4位随机数"""
- timestamp = str(int(time.time() * 1000))
- rand = ''.join(random.choices(string.digits, k=4))
- return f'TX{timestamp}{rand}'
-
-
-class TixianShenqingV3View(APIView):
- """
- 自动提现收款接口(新流程第二步)
- POST /yonghu/tixiansq
- 必须传 shenhe_danhao;审核状态须为 6=待收款;申请时已扣款,本接口不再扣款
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- shenhe_danhao = (request.data.get('shenhe_danhao') or '').strip()
- if shenhe_danhao:
- from .tixian_shenhe_views import process_audit_collect
- return process_audit_collect(request)
- return Response({
- 'code': 7,
- 'msg': '缺少审核单号,请先提交提现申请(/yonghu/zddksh)并等待审核通过后再收款',
- })
-
-
-
-
-
-
-
-from utils.wechat_v3 import verify_wechat_sign, decrypt_callback_ciphertext
-
-class TixianCallbackV3View(APIView):
- """
- 微信转账结果回调接口(最终状态通知)
- 处理微信异步发送的转账成功/失败结果
- """
- permission_classes = [] # 无需认证,签名验证保证安全
-
- def post(self, request):
- headers = request.headers
- body = request.body.decode('utf-8')
-
- # 1. 验证签名
- if not verify_wechat_sign(headers, body):
- return Response({'code': 'FAIL', 'message': '签名验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 解密数据
- try:
- data = json.loads(body)
- resource = data.get('resource')
- if not resource:
- return Response({'code': 'FAIL', 'message': 'resource不存在'}, status=status.HTTP_400_BAD_REQUEST)
-
- associated_data = resource.get('associated_data', '')
- nonce = resource.get('nonce')
- ciphertext = resource.get('ciphertext')
- if not nonce or not ciphertext:
- return Response({'code': 'FAIL', 'message': '解密字段缺失'}, status=status.HTTP_400_BAD_REQUEST)
-
- plaintext = decrypt_callback_ciphertext(associated_data, nonce, ciphertext)
- callback_data = json.loads(plaintext)
- except Exception as e:
- logger.error(f"回调解密失败: {str(e)}", exc_info=True)
- return Response({'code': 'FAIL', 'message': f'解密失败: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 获取关键信息
- out_bill_no = callback_data.get('out_bill_no') # 商户提现单号(即 tixian_id)
- transfer_status = callback_data.get('transfer_status') # SUCCESS / FAIL
- wechat_transfer_no = callback_data.get('transfer_no') # 微信转账单号
- fail_reason = callback_data.get('fail_reason', '')
-
- if not out_bill_no:
- return Response({'code': 'FAIL', 'message': 'out_bill_no缺失'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 幂等处理:只更新状态为0(处理中)的记录,已最终状态不再处理
- with transaction.atomic():
- try:
- auto_record = TixianAutoRecord.objects.select_for_update().get(tixian_id=out_bill_no)
- except TixianAutoRecord.DoesNotExist:
- # 记录不存在,视为已处理,返回成功
- return Response({'code': 'SUCCESS', 'message': 'ok'})
-
- # 如果已经是最新状态,直接返回成功(幂等)
- if auto_record.zhuangtai != 0:
- return Response({'code': 'SUCCESS', 'message': 'already processed'})
-
- if transfer_status == 'SUCCESS':
- from .tixian_shenhe_services import mark_transfer_success
- mark_transfer_success(
- auto_record,
- wechat_transfer_no=wechat_transfer_no,
- with_accounting=True,
- )
-
- else: # transfer_status == 'FAIL'
- auto_record.zhuangtai = 2
- auto_record.fail_reason = fail_reason or '微信转账失败'
- auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
-
- if not auto_record.shenhe_danhao:
- logger.error('回调失败但打款记录无审核单号 bill=%s', out_bill_no)
- else:
- # 申请时已扣款,回调失败不退余额,恢复待收款(6)可重试
- from .models import TixianShenheJilu
- from .tixian_shenhe_services import (
- release_collect_quota_for_record,
- sync_audit_and_jilu_status,
- )
- try:
- audit = TixianShenheJilu.objects.select_for_update().get(
- shenhe_danhao=auto_record.shenhe_danhao
- )
- audit.tixian_auto_id = None
- audit.fail_reason = auto_record.fail_reason
- sync_audit_and_jilu_status(audit, 6, fail_reason=auto_record.fail_reason)
- except TixianShenheJilu.DoesNotExist:
- logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}')
- release_collect_quota_for_record(auto_record)
-
- return Response({'code': 'SUCCESS', 'message': 'ok'})
-
-
-
-class TixianQueRenAutoView(APIView):
- """
- 用户确认提现结果
- POST /yonghu/tixianqr
- 新审核流程:打款记录带 shenhe_danhao 时,确认成功同步审核表+提现记录表为 2;取消不退款(申请已扣),恢复待收款 6 可重试
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- tixian_id = request.data.get('tixian_id')
- result = request.data.get('result') # 1成功 0失败/取消
-
- if not tixian_id or result is None:
- return Response({'code': 1, 'msg': '缺少参数'}, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- result = int(result)
- except ValueError:
- return Response({'code': 2, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
-
- if result not in [0, 1]:
- return Response({'code': 3, 'msg': 'result值无效'}, status=status.HTTP_400_BAD_REQUEST)
-
- user_main = request.user
-
- with transaction.atomic():
- try:
- auto_record = TixianAutoRecord.objects.select_for_update().get(
- tixian_id=tixian_id,
- yonghuid=user_main.yonghuid,
- )
- except TixianAutoRecord.DoesNotExist:
- return Response({'code': 4, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- if result == 1:
- if auto_record.zhuangtai == 1:
- return Response({'code': 0, 'msg': '提现成功', 'data': None})
-
- if auto_record.zhuangtai != 0:
- return Response({'code': 5, 'msg': '该提现已处理'})
-
- from .tixian_shenhe_services import mark_transfer_success
- mark_transfer_success(auto_record, with_accounting=True)
-
- return Response({'code': 0, 'msg': '提现成功', 'data': None})
-
- # 用户取消或失败
- auto_record.zhuangtai = 2
- auto_record.fail_reason = '用户取消收款'
- auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
-
- if not auto_record.shenhe_danhao:
- logger.error('确认取消但打款记录无审核单号 bill=%s', tixian_id)
- return Response({'code': 5, 'msg': '该提现已处理'})
-
- # 申请时已扣款,取消不退余额,审核单恢复 6 待收款,可再次点收款
- from .models import TixianShenheJilu
- from .tixian_shenhe_services import (
- release_collect_quota_for_record,
- sync_audit_and_jilu_status,
- )
- try:
- audit = TixianShenheJilu.objects.select_for_update().get(
- shenhe_danhao=auto_record.shenhe_danhao,
- )
- audit.tixian_auto_id = None
- sync_audit_and_jilu_status(
- audit, 6,
- fail_reason='用户取消收款,可重新发起',
- )
- except TixianShenheJilu.DoesNotExist:
- logger.warning(f'确认取消但审核记录不存在: {auto_record.shenhe_danhao}')
- release_collect_quota_for_record(auto_record)
-
- return Response({'code': 0, 'msg': '提现已取消', 'data': None})
-
- except Exception as e:
- logger.error(f'提现确认异常: {str(e)}', exc_info=True)
- return Response({'code': 99, 'msg': '系统错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
+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 utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
+from utils.fadan_utils import check_fadan_qiangdan_eligible
+from utils.wechat_v3 import verify_wechat_sign, decrypt_callback_ciphertext
+
+from Django.utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
+from users.fadan_fenhong_utils import process_fadan_fenhong
+
+from orders.utils import (
+ update_daily_payout, update_daily_dispatch_stat,
+ update_daily_income, 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,
+ verify_kefu_permission
+)
+from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
+
+from .models import (
+ UserMain, UserBoss, UserDashou, UserShangjia, UserGuanshi,
+ UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
+ TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
+ RankingRecord
+)
+from .tixian_shenhe_services import (
+ load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
+ mark_transfer_success, release_collect_quota_for_record
+)
+from .tixian_shenhe_views import process_audit_collect
+
+from orders.models import (
+ Dingdan, DingdanPingtai, DingdanShangjia, Lilubiao,
+ Chufajilu, Chufatupian, Tuikuanjilu, Dashoutupian,
+ PreSettlement, Fadan, FadanShensuTupian
+)
+from products.models import (
+ ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
+)
+from config.models import Qunpeizhi, Szjilu
+from backend.models import ShangjiaRiTongji
+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 orders.utils import create_shenhe_jilu_from_temp
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+
+
+
+
+
+
+# 获取用户代理信息(用于更详细的追踪)
+def huoquYonghuDaili(request):
+ """
+ 获取用户浏览器/设备信息
+ """
+ user_agent = request.META.get('HTTP_USER_AGENT', '')
+
+ # 这里可以添加更复杂的用户代理解析
+ # 为简化,我们只返回原始字符串的前100个字符
+ return user_agent[:100] if user_agent else ''
+
+
+
+
+
+def huoquKehuduanIP(request):
+ """
+ 强化版:安全、兼容地获取客户端真实IP地址
+ """
+ ip = None
+
+ # 1. 从标准头部获取IP链
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ if x_forwarded_for:
+ # 按逗号分割,取第一个(即最原始的客户端IP)
+ ip = x_forwarded_for.split(',')[0].strip()
+
+ # 2. 如果上面没有,则用Nginx传递的X-Real-IP
+ if not ip:
+ ip = request.META.get('HTTP_X_REAL_IP')
+
+ # 3. 最后降级到连接地址
+ if not ip:
+ ip = request.META.get('REMOTE_ADDR')
+
+ # ===== 核心:清洗和验证IP格式 =====
+ if ip:
+ # 情况A: 处理IPv4映射的IPv6地址 (::ffff:192.168.1.1)
+ if ip.startswith('::ffff:'):
+ ip = ip[7:]
+
+ # 情况B: 处理带端口的IPv4地址 (223.91.110.123:62417)
+ # 这是导致您记录“三段”的最可能原因!
+ # 如果IP字符串中有冒号,且最后一个冒号前的部分是IPv4,则分离端口。
+ if ':' in ip and '.' in ip:
+ # 尝试按冒号分割,取第一部分
+ possible_ip = ip.rsplit(':', 1)[0] # 从右边分割一次
+ # 验证它是否是有效的IPv4地址
+ try:
+ ipaddress.IPv4Address(possible_ip) # 如果通过验证,说明格式正确
+ ip = possible_ip
+ except (ipaddress.AddressValueError, ValueError):
+ # 如果验证失败,说明可能不是IPv4带端口,保持原样或进行其他处理
+ pass
+
+ # 情况C: 验证最终结果是否为有效IPv4(可选,但推荐)
+ try:
+ # 此步骤会抛出异常,如果ip变量此时仍是“223.91.110.”这种格式
+ ipaddress.IPv4Address(ip)
+ except (ipaddress.AddressValueError, ValueError):
+ # 记录一个警告日志,并将IP标记为无效或存为原始字符串
+ print(f"警告:获取到无效或非标准IP地址格式: {ip}")
+ # 您可以选择在此处 return None 或存储原始字符串以供排查
+
+ return ip
+
+
+
+
+class WechatMiniProgramLoginView(APIView):
+ """
+ 微信小程序登录接口
+ """
+ throttle_classes = [AnonRateThrottle]
+ permission_classes = [AllowAny]
+
+ def post(self, request):
+ try:
+ code = request.data.get('code', '').strip()
+ if not code:
+ return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取微信openid和unionid
+ wechat_data = self.get_wechat_openid(code)
+ if not wechat_data or 'openid' not in wechat_data:
+ error_msg = wechat_data.get('errmsg', '微信授权失败')
+ return Response({'code': 2, 'msg': f'微信登录失败: {error_msg}', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ openid = wechat_data['openid']
+ session_key = wechat_data.get('session_key', '')
+ unionid = wechat_data.get('unionid', '') # 🆕【新增】获取unionid
+
+ # 获取客户端真实IP
+ kehuduan_ip = self.huoquKehuduanIP(request)
+
+ with transaction.atomic():
+ # 创建或获取用户
+ user_main, created = UserMain.objects.select_for_update().get_or_create(
+ openid=openid,
+ defaults={'yonghuid': self.shengchengYonghuID(), 'user_type': 'normal'}
+ )
+
+ # 🆕【新增】保存unionid
+ if unionid and unionid != user_main.unionid:
+ user_main.unionid = unionid
+
+ cunchu_ip = kehuduan_ip
+ user_main.ip = cunchu_ip
+ user_main.last_login_time = timezone.now()
+ user_main.save()
+
+ if created:
+ UserBoss.objects.create(user=user_main, nickname='板板大人')
+
+ # 检查扩展表
+ dashou_status = 0
+ shangjia_status = 0
+ guanshi_status = 0
+ boss_nickname = '微信用户'
+
+ try:
+ boss_profile = UserBoss.objects.get(user=user_main)
+ boss_nickname = boss_profile.nickname if boss_profile.nickname else '微信用户'
+ except UserBoss.DoesNotExist:
+ pass
+
+ try:
+ UserDashou.objects.get(user=user_main)
+ dashou_status = 1
+ except UserDashou.DoesNotExist:
+ dashou_status = None
+
+ try:
+ UserShangjia.objects.get(user=user_main)
+ shangjia_status = 1
+ except UserShangjia.DoesNotExist:
+ shangjia_status = None
+
+ try:
+ UserGuanshi.objects.get(user=user_main)
+ guanshi_status = 1
+ except UserGuanshi.DoesNotExist:
+ guanshi_status = None
+
+
+ try:
+ UserZuzhang.objects.get(user=user_main)
+ zuzhang_status = 1
+ except UserZuzhang.DoesNotExist:
+ zuzhang_status = None
+
+
+ # 在检查 zuzhang 扩展表之后,插入以下代码
+ try:
+ kg = UserShenheguan.objects.get(user=user_main)
+ kaoheguan_status = 1 if kg.zhuangtai == 1 else 0
+ except UserShenheguan.DoesNotExist:
+ kaoheguan_status = 0
+
+ # 生成JWT token
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+
+ # 计算订单数量
+ order_counts = self.jisuanDingdanShuliang(user_main.yonghuid)
+
+ # 获取群配置
+ group_info = self.huoquQunPeizhi()
+
+ # 返回数据
+ response_data = {
+ 'token': token,
+ 'nicheng': boss_nickname,
+ 'uid': user_main.yonghuid,
+ 'touxiang': user_main.avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ 'zuzhangstatus': zuzhang_status,
+ 'kaoheguanstatus': kaoheguan_status,
+ 'dingdantiaoshu': order_counts,
+ 'dashouqun': group_info.get('dashouqun', ''),
+ 'dashouqunid': group_info.get('dashouqunid', ''),
+ 'guanshiqun': group_info.get('guanshiqun', ''),
+ 'guanshiqunid': group_info.get('guanshiqunid', '')
+ }
+
+ return Response({'code': 0, 'msg': '登录成功', 'data': response_data})
+
+ except Exception as e:
+ error_openid = locals().get('openid', 'N/A')
+ logger.error(f"微信登录异常 - openid: {error_openid}, 错误类型: {type(e).__name__}, 错误详情: {str(e)}",
+ exc_info=True)
+
+ return Response({'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def get_wechat_openid(self, code):
+ """
+ 调用微信接口获取openid和unionid
+ """
+ try:
+ appid = getattr(settings, 'WEIXIN_APPID', '')
+ secret = getattr(settings, 'WEIXIN_SECRET', '')
+
+ if not appid or not secret:
+ raise ValueError('微信配置未设置')
+
+ url = 'https://api.weixin.qq.com/sns/jscode2session'
+ params = {'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code'}
+
+ response = requests.get(url, params=params, timeout=10)
+ result = response.json()
+
+ if 'openid' in result:
+ return {'openid': result['openid'], 'session_key': result.get('session_key', ''),
+ 'unionid': result.get('unionid', '')}
+ else:
+ errcode = result.get('errcode', 'unknown')
+ errmsg = result.get('errmsg', '未知错误')
+ return {'errmsg': f'[{errcode}]{errmsg}'}
+
+ except requests.exceptions.Timeout:
+ return {'errmsg': '请求微信接口超时'}
+ except Exception as e:
+ return {'errmsg': f'请求微信接口异常: {str(e)}'}
+
+ def shengchengYonghuID(self):
+ """
+ 生成7位数字用户ID
+ """
+ for _ in range(10):
+ timestamp_part = str(int(time.time()))[-5:].zfill(5)
+ random_part = str(random.randint(0, 99)).zfill(2)
+ user_id = timestamp_part + random_part
+
+ if len(user_id) == 7 and user_id.isdigit():
+ if not UserMain.objects.filter(yonghuid=user_id).exists():
+ return user_id
+
+ raise Exception('生成用户ID失败')
+
+ def huoquKehuduanIP(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
+
+ ip = request.META.get('REMOTE_ADDR', '')
+ return ip if ip else '0.0.0.0'
+
+ def jisuanDingdanShuliang(self, yonghuid):
+ """
+ 统计订单数量
+ """
+ try:
+ if not DingdanPingtai.objects.filter(laoban_id=yonghuid).exists():
+ return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0}
+
+ dingdan_ids = DingdanPingtai.objects.filter(laoban_id=yonghuid).values_list('dingdan__id', flat=True)
+
+ results = Dingdan.objects.filter(id__in=dingdan_ids).aggregate(
+ daifuwu=Count(Case(When(Q(zhuangtai=1) | Q(zhuangtai=7), then=1), output_field=IntegerField())),
+ fuwuzhong=Count(Case(When(zhuangtai=2, then=1), output_field=IntegerField())),
+ yiwancheng=Count(Case(When(zhuangtai=3, then=1), output_field=IntegerField())),
+ yituikuan=Count(Case(When(zhuangtai=5, then=1), output_field=IntegerField()))
+ )
+
+ return {
+ 'daifuwu': results['daifuwu'] or 0,
+ 'fuwuzhong': results['fuwuzhong'] or 0,
+ 'yiwancheng': results['yiwancheng'] or 0,
+ 'yituikuan': results['yituikuan'] or 0
+ }
+
+ except Exception:
+ return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0}
+
+ def huoquQunPeizhi(self):
+ """
+ 获取群配置信息
+ """
+ try:
+ qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2])
+
+ result = {
+ 'dashouqun': '',
+ 'dashouqunid': '',
+ 'guanshiqun': '',
+ 'guanshiqunid': ''
+ }
+
+ for config in qun_configs:
+ if config.id == 1:
+ result['dashouqun'] = config.neirong or ''
+ result['dashouqunid'] = config.qunid or ''
+ elif config.id == 2:
+ result['guanshiqun'] = config.neirong or ''
+ result['guanshiqunid'] = config.qunid or ''
+
+ return result
+
+ except Exception:
+ return {
+ 'dashouqun': '',
+ 'dashouqunid': '',
+ 'guanshiqun': '',
+ 'guanshiqunid': ''
+ }
+
+# views/weixin_official.py - 完整文件
+class WeixinOfficialCallbackView(APIView):
+ """
+ 微信服务号回调接口
+ 处理用户关注、取消关注等事件
+ 核心:有关注事件就存,不管有没有UnionID
+ """
+ throttle_classes = []
+ permission_classes = [AllowAny]
+ authentication_classes = []
+
+ def get(self, request):
+ """验证服务器配置(微信官方要求)"""
+ try:
+ signature = request.GET.get('signature', '')
+ timestamp = request.GET.get('timestamp', '')
+ nonce = request.GET.get('nonce', '')
+ echostr = request.GET.get('echostr', '')
+
+ token = getattr(settings, 'WEIXIN_OFFICIAL_TOKEN', '')
+
+ if not token:
+ logger.error("服务号Token未配置")
+ return HttpResponse('Token not configured', status=500)
+
+ # 验证签名
+ tmp_list = sorted([token, timestamp, nonce])
+ tmp_str = ''.join(tmp_list)
+ hash_str = hashlib.sha1(tmp_str.encode()).hexdigest()
+
+ if hash_str == signature:
+ return HttpResponse(echostr)
+ else:
+ logger.error(f"签名验证失败")
+ return HttpResponse('Signature verification failed', status=403)
+
+ except Exception as e:
+ logger.error(f"验证服务器异常: {str(e)}")
+ return HttpResponse('Internal Server Error', status=500)
+
+ def post(self, request):
+ """处理事件推送(关注、取关)"""
+ try:
+ xml_str = request.body.decode('utf-8')
+ logger.info(f"收到服务号事件,原始数据(前200字符): {xml_str[:200]}")
+
+ root = ET.fromstring(xml_str)
+
+ msg_type = root.find('MsgType').text
+ from_user = root.find('FromUserName').text # 服务号OpenID
+
+ if msg_type == 'event':
+ event = root.find('Event').text
+
+ if event == 'subscribe':
+ return self.handle_subscribe_event(root, from_user)
+ elif event == 'unsubscribe':
+ return self.handle_unsubscribe_event(root, from_user)
+ else:
+ # 其他事件(如菜单点击)直接返回success
+ return HttpResponse('success')
+
+ # 非事件消息也直接返回success
+ return HttpResponse('success')
+
+ except Exception as e:
+ logger.error(f"处理服务号事件异常: {str(e)}", exc_info=True)
+ # 即使异常也返回success,避免微信服务器重试
+ return HttpResponse('success')
+
+ def handle_subscribe_event(self, root, from_user):
+ """处理用户关注事件 - 简化版:有就存,没有拉倒"""
+ try:
+ # 尝试获取UnionID(有就有,没有也没关系)
+ unionid_elem = root.find('UnionId')
+ unionid = unionid_elem.text if unionid_elem is not None else None
+
+ logger.info(f"用户关注,OpenID: {from_user}, UnionID: {unionid}")
+
+ # 核心逻辑:更新或创建记录
+ obj, created = OfficialAccountUser.objects.update_or_create(
+ official_openid=from_user,
+ defaults={
+ 'unionid': unionid,
+ 'is_subscribed': True,
+ 'subscribe_time': timezone.now()
+ }
+ )
+
+ action = "新建" if created else "更新"
+ logger.info(f"{action}用户记录成功: OpenID={from_user}")
+
+ # 回复欢迎消息(可选)
+ return self.generate_text_response(root, "感谢关注星阙网络服务!平台有新订单时您会收到通知。")
+
+ except Exception as e:
+ logger.error(f"处理关注事件异常: {str(e)}", exc_info=True)
+ # 依然返回success,不让微信重试
+ return HttpResponse('success')
+
+ def handle_unsubscribe_event(self, root, from_user):
+ """处理用户取消关注事件 - 简化版:标记为未订阅"""
+ try:
+ updated_count = OfficialAccountUser.objects.filter(
+ official_openid=from_user
+ ).update(is_subscribed=False)
+
+ if updated_count > 0:
+ logger.info(f"用户取消关注,已标记为未订阅: {from_user}")
+ else:
+ logger.info(f"用户取消关注,但记录不存在(无需处理): {from_user}")
+
+ return HttpResponse('success')
+
+ except Exception as e:
+ logger.error(f"处理取消关注事件异常: {str(e)}", exc_info=True)
+ return HttpResponse('success')
+
+ def generate_text_response(self, root, content):
+ """生成文本消息响应"""
+ try:
+ to_user = root.find('ToUserName').text
+ from_user = root.find('FromUserName').text
+ response_xml = f"""
+
+
+{int(time.time())}
+
+
+"""
+ return HttpResponse(response_xml, content_type='application/xml')
+
+ except Exception as e:
+ logger.error(f"生成回复消息异常: {str(e)}")
+ # 即使生成回复失败,也返回一个空的success
+ return HttpResponse('success')
+
+
+
+
+# yonghu/views.py
+
+class UserInfoUpdateView(APIView):
+ permission_classes = [permissions.IsAuthenticated]
+
+ # 进程内缓存,防止同一code被重复调用微信(单进程安全,多进程小概率重复,但用户极少可忽略)
+ _phone_cache = {}
+
+ def post(self, request):
+ try:
+ user = request.user
+ boss = self._get_or_create_boss(user)
+ result = {}
+
+ # 头像
+ av = request.FILES.get('avatar')
+ if av:
+ ok, msg = validate_image(av)
+ if not ok:
+ return Response({'code': 1, 'msg': msg, 'data': None}, status=400)
+ url = self._upload_avatar(user, av)
+ if url:
+ result['touxiang'] = url
+ user.avatar = url
+ else:
+ return Response({'code': 2, 'msg': '头像上传失败', 'data': None}, status=500)
+
+ # 昵称
+ nick = request.data.get('nicheng')
+ if nick:
+ nick = str(nick).strip()
+ if len(nick) > 20:
+ return Response({'code': 3, 'msg': '昵称过长', 'data': None}, status=400)
+ boss.nickname = nick
+ result['nicheng'] = nick
+
+ # 手机号 —— 极简可靠版
+ code = request.data.get('shoujihao_code')
+ if code:
+ phone = self._get_phone_safe(code)
+ if phone is None:
+ return Response({'code': 6, 'msg': '手机号获取失败,请重新授权', 'data': None}, status=400)
+ user.phone = phone
+ user.shoujihao_renzheng = True
+ result['phone'] = phone
+
+ if not result:
+ return Response({'code': 5, 'msg': '无更新', 'data': None}, status=400)
+
+ with transaction.atomic():
+ user.save()
+ boss.save()
+ return Response({'code': 0, 'msg': '更新成功', 'data': result})
+
+ except Exception as e:
+ logger.exception("系统异常")
+ return Response({'code': 99, 'msg': str(e), 'data': None}, status=500)
+
+ # ---------- 安全手机号(内存去重) ----------
+ def _get_phone_safe(self, code):
+ # 如果已经处理过这个code,直接返回结果
+ if code in self._phone_cache:
+ cached = self._phone_cache[code]
+ return cached if cached != '__FAIL__' else None
+
+ # 标记处理中(简单防重入)
+ self._phone_cache[code] = '__PROCESSING__'
+ try:
+ phone = self._call_wechat(code)
+ self._phone_cache[code] = phone
+ return phone
+ except Exception as e:
+ logger.error(f"微信调用失败: {str(e)}")
+ self._phone_cache[code] = '__FAIL__'
+ return None
+
+ # ---------- 微信接口调用(每次全新token) ----------
+ def _call_wechat(self, code):
+ token = self._get_new_token()
+ url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
+ resp = requests.post(url, json={'code': code}, timeout=10)
+ data = resp.json()
+ errcode = data.get('errcode')
+ if errcode == 0:
+ return data['phone_info']['purePhoneNumber']
+ # 即使刚拿的token也可能意外失效,重试一次
+ if errcode == 40001:
+ token = self._get_new_token()
+ url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
+ resp = requests.post(url, json={'code': code}, timeout=10)
+ data = resp.json()
+ if data.get('errcode') == 0:
+ return data['phone_info']['purePhoneNumber']
+ raise Exception(f"微信错误(errcode={errcode}): {data.get('errmsg')}")
+
+ # ---------- 获取全新 access_token(不缓存,每次调微信) ----------
+ def _get_new_token(self):
+ appid = getattr(settings, 'WEIXIN_APPID', '')
+ secret = getattr(settings, 'WEIXIN_SECRET', '')
+ if not appid or not secret:
+ raise Exception('缺少微信配置')
+ resp = requests.get('https://api.weixin.qq.com/cgi-bin/token', params={
+ 'grant_type': 'client_credential',
+ 'appid': appid,
+ 'secret': secret
+ })
+ data = resp.json()
+ token = data.get('access_token')
+ if token:
+ return token
+ raise Exception(f"获取access_token失败: {data}")
+
+ # ---------- 工具 ----------
+ def _get_or_create_boss(self, user):
+ try:
+ return UserBoss.objects.get(user=user)
+ except UserBoss.DoesNotExist:
+ return UserBoss.objects.create(user=user, nickname='微信用户')
+
+ def _upload_avatar(self, user, file):
+ try:
+ uid = user.yonghuid
+ if not uid: return None
+ old = user.avatar
+ if old: delete_from_oss(old)
+ ext = os.path.splitext(file.name)[1] or '.jpg'
+ name = f"{uuid.uuid4().hex}{ext}"
+ path = f"avatar/{uid}/{name}"
+ return path if upload_to_oss(file, path) else None
+ except:
+ return None
+
+
+
+# yonghu/views.py 或 yonghu/api_views.py
+
+class DashouXinxiAPIView(APIView):
+ """
+ 获取打手信息接口
+ 请求方式:POST
+ 认证方式:JWT Token
+ 返回:打手基本信息、资产信息、会员信息,金牌打手额外返回专属字段
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 获取当前登录用户
+ current_user = request.user
+ yonghuid = current_user.yonghuid
+
+ # 2. 查询打手扩展表
+ try:
+ dashou_profile = current_user.dashou_profile
+ except UserDashou.DoesNotExist:
+ return Response({
+ 'code': 400,
+ 'message': '您不是打手身份',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 构建基础信息响应(原有字段完全保留)
+ data = {
+ 'dashounicheng': dashou_profile.nicheng or '',
+ 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'dashouzhuangtai': dashou_profile.zhuangtai,
+ 'yongjin': str(dashou_profile.yue) if dashou_profile.yue is not None else '0.00',
+ 'zonge': str(dashou_profile.zonge) if dashou_profile.zonge is not None else '0.00',
+ 'yajin': str(dashou_profile.yajin) if dashou_profile.yajin is not None else '0.00',
+ 'jiedanZongliang':dashou_profile.jiedanzongliang if dashou_profile.jiedanzongliang is not None else '0',
+ 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00',
+ 'chengjiaoliang': dashou_profile.chengjiaozongliang,
+ 'jifen': dashou_profile.jifen,
+ 'chenghao': dashou_profile.chenghao or '',
+ }
+
+ # 4. 查询会员信息(原有逻辑)
+ huiyuan_list = []
+ try:
+ huiyuan_records = Huiyuangoumai.objects.filter(
+ yonghu_id=yonghuid
+ ).select_related()
+
+ for record in huiyuan_records:
+ # 检查会员是否过期
+ is_expired = record.jiance_shifou_daoqi()
+
+ # 只返回未过期的会员(huiyuan_zhuangtai = 1)
+ if record.huiyuan_zhuangtai == 1:
+ huiyuan_list.append({
+ 'huiyuanid': record.huiyuan_id,
+ 'huiyuanming': record.jieshao or f"会员{record.huiyuan_id}",
+ 'daoqi': record.daoqi_time
+ })
+ except Exception as e:
+ # 会员信息查询失败不影响主要功能,返回空列表
+ huiyuan_list = []
+
+ data['clumber'] = huiyuan_list
+
+ # 5. 如果是金牌打手,添加专属字段
+ if dashou_profile.chenghao == "金牌打手":
+ data.update({
+ 'jiedanZongliang': dashou_profile.jiedanzongliang, # 接单总量
+ 'jinriJiedan': str(dashou_profile.jinrishouyi) if dashou_profile.jinrishouyi is not None else '0.00', # 今日接单金额
+ 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', # 最近提现金额(今日已提现)
+ })
+
+ # 6. 返回成功响应
+ return Response({
+ 'code': 200,
+ 'message': '获取成功',
+ 'data': data
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ # 生产环境应使用 logging 记录错误,而不是 print
+ # logger.error(f"获取打手信息失败: {e}", exc_info=True)
+ return Response({
+ 'code': 500,
+ 'message': f'服务器内部错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class HuoQuYaoQingRenView(APIView):
+ """
+ 获取当前打手的邀请人信息
+ POST /yonghu/hqwdyqr
+ Headers: Authorization: Bearer
+ 返回邀请人的用户ID、昵称(来自老板扩展表)、头像相对URL
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ current_user = request.user # 当前登录用户
+ yonghuid = current_user.yonghuid # 7位用户ID
+
+ # 1. 校验打手身份及账号状态(只检查账号状态,不检查接单状态)
+ try:
+ dashou = UserDashou.objects.get(user=current_user)
+ if dashou.zhanghaozhuangtai != 1:
+ return Response({
+ 'code': 403,
+ 'message': '您的账号已被封禁,无法查看邀请人',
+ 'data': None
+ })
+ except UserDashou.DoesNotExist:
+ return Response({
+ 'code': 404,
+ 'message': '您还不是打手,无法查看邀请人',
+ 'data': None
+ })
+
+ # 2. 获取邀请人ID
+ yaoqingren_id = dashou.yaoqingren
+ if not yaoqingren_id:
+ return Response({
+ 'code': 404,
+ 'message': '您没有邀请人',
+ 'data': None
+ })
+
+ # 3. 查询邀请人主表(头像)
+ try:
+ inviter_main = UserMain.objects.get(yonghuid=yaoqingren_id)
+ touxiang = inviter_main.avatar or ''
+ except UserMain.DoesNotExist:
+ return Response({
+ 'code': 404,
+ 'message': '邀请人信息不存在',
+ 'data': None
+ })
+
+ # 4. 查询老板扩展表(昵称),如果没有则使用空字符串
+ nicheng = ''
+ try:
+ boss = UserBoss.objects.get(user=inviter_main)
+ nicheng = boss.nickname or ''
+ except UserBoss.DoesNotExist:
+ pass # 邀请人可能不是老板,昵称留空
+
+ response_data = {
+ 'uid': yaoqingren_id,
+ 'nicheng': nicheng,
+ 'touxiang': touxiang # 相对路径,前端自行拼接
+ }
+
+ return Response({
+ 'code': 200,
+ 'message': '获取成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ logger.error(f'获取邀请人失败: {str(e)}', exc_info=True)
+ return Response({
+ 'code': 500,
+ 'message': '系统繁忙,请稍后重试',
+ 'data': None
+ })
+
+
+
+
+# yonghu/views.py 或 yonghu/api_views.py
+
+class ZaixianZhuangtaiAPIView(APIView):
+ """
+ 更改打手在线状态接口
+ 请求方式:POST
+ 认证方式:JWT Token
+ 请求参数:status(0=离线,1=在线)
+ 返回:更新后的状态信息
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 获取请求参数
+ new_status = request.data.get('status')
+
+ if new_status is None:
+ return Response({
+ 'code': 400,
+ 'message': '缺少状态参数',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 转换为整数,确保是0或1
+ try:
+ new_status = int(new_status)
+ if new_status not in [0, 1]:
+ return Response({
+ 'code': 400,
+ 'message': '状态参数必须是0或1',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+ except ValueError:
+ return Response({
+ 'code': 400,
+ 'message': '状态参数必须是整数',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 获取当前登录用户
+ current_user = request.user
+ yonghuid = current_user.yonghuid
+
+
+
+ # 3. 查询打手扩展表
+ try:
+ dashou_profile = current_user.dashou_profile
+ except UserDashou.DoesNotExist:
+
+ return Response({
+ 'code': 400,
+ 'message': '您不是打手身份',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 检查账号状态(是否被封禁)
+ if dashou_profile.zhanghaozhuangtai != 1:
+
+ return Response({
+ 'code': 403,
+ 'message': '账号状态异常,无法更改在线状态',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 5. 更新在线状态
+ old_status = dashou_profile.zaixianzhuangtai
+
+ # 如果状态没有变化,直接返回成功
+ if old_status == new_status:
+
+ return Response({
+ 'code': 200,
+ 'message': '状态未变化',
+ 'data': {
+ 'zaixianzhuangtai': new_status,
+ 'status_text': '在线' if new_status == 1 else '离线'
+ }
+ }, status=status.HTTP_200_OK)
+
+ # 更新状态
+ dashou_profile.zaixianzhuangtai = new_status
+ dashou_profile.save()
+
+
+
+ # 6. 返回成功响应
+ return Response({
+ 'code': 200,
+ 'message': '状态更新成功',
+ 'data': {
+ 'zaixianzhuangtai': new_status,
+ 'status_text': '在线' if new_status == 1 else '离线'
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+
+ return Response({
+ 'code': 500,
+ 'message': f'服务器内部错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class GuanshiXinxiView(APIView):
+ """
+ 获取管事信息接口
+ 路径:/yonghu/guanshixinxi
+ 方法:POST
+ 权限:JWT认证
+ 功能:获取当前管事的详细信息,包括基本信息、分红信息以及已充值打手数量
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 获取当前用户(通过JWT认证)
+ current_user = request.user
+
+ # 查询管事扩展表
+ try:
+ guanshi_profile = UserGuanshi.objects.get(user=current_user)
+
+ # 🔥【新增】查询已充值打手数量(从分红表中统计)
+ # Gsfenhong 表中 guanshi 字段存储管事ID(yonghuid)
+ yichongzhi_count = Gsfenhong.objects.filter(guanshi=current_user.yonghuid).count()
+
+ # 返回管事信息
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': {
+ 'gszhstatus': guanshi_profile.zhuangtai, # 管事账号状态
+ 'yaoqingzongshu': guanshi_profile.yaogingshuliang, # 邀请打手总数
+ 'fenyongzonge': float(guanshi_profile.chongzhifenrun), # 充值分佣总额
+ 'fenyongtixian': float(guanshi_profile.yue), # 可提现余额
+ 'yichongzhiDashou': yichongzhi_count, # 已充值打手数量
+ }
+ })
+
+ except UserGuanshi.DoesNotExist:
+ # 用户不是管事,返回错误
+ return Response({
+ 'code': 400,
+ 'msg': '您还不是管事,请先注册',
+ 'data': None
+ }, status=400)
+
+ except Exception as e:
+ # 捕获所有异常,返回错误信息并记录日志
+ logger.error(f"获取管事信息异常: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 500,
+ 'msg': f'获取信息失败: {str(e)}',
+ 'data': None
+ }, status=500)
+
+
+
+
+# yonghu/views.py
+class ShangJiaXinXiView(APIView):
+ """
+ 获取商家信息接口 (POST /yonghu/shangjiaxinxi)
+ 权限:需JWT认证
+ 返回:商家基本信息 + 称号列表 + 今日/本月统计
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 获取商家扩展资料
+ shangjia = request.user.shop_profile
+ except ObjectDoesNotExist:
+ return Response({
+ 'code': 4001,
+ 'msg': '您还未注册成为商家或商家资料不存在',
+ 'data': None
+ }, status=status.HTTP_200_OK)
+
+ user = request.user
+ yonghuid = user.yonghuid
+ today = timezone.now().date()
+ this_month_start = today.replace(day=1)
+
+ # ------------------- 1. 从 UserShangjia 获取基础字段 -------------------
+ response_data = {
+ 'sjzhzhuangtai': shangjia.zhuangtai, # 商家账号状态
+ 'fadanzong': shangjia.fabu, # 发单总量(累计)
+ 'tuikuanzong': shangjia.tuikuan, # 退款总量(累计)
+ 'sjyue': float(shangjia.yue), # 商家余额
+ # 'riliushui' 和 'yueliushui' 将从统计表计算,不再使用原字段
+ }
+
+ # ------------------- 2. 获取称号列表(仅限商家称号) -------------------
+ chenghao_list = []
+ # 查询当前用户的所有称号关联
+ yonghu_chenghao_qs = YonghuChenghao.objects.filter(yonghu=user).select_related('chenghao')
+ for yc in yonghu_chenghao_qs:
+ chenghao_obj = yc.chenghao
+ # 只保留类型为“商家”的称号
+ if chenghao_obj.leixing == 'shangjia':
+ chenghao_list.append({
+ 'mingcheng': chenghao_obj.mingcheng,
+ 'texiao_json': chenghao_obj.texiao_miaoshu, # 前端需要的特效描述
+ # 如果还需要其他字段(如图标URL)可在此添加,打手端有 texiao_json
+ })
+ response_data['chenghao_list'] = chenghao_list
+
+ # ------------------- 3. 从 ShangjiaRiTongji 获取今日统计 -------------------
+ try:
+ today_stat = ShangjiaRiTongji.objects.get(yonghuid=yonghuid, riqi=today)
+ jinri_paifa_dingdan = today_stat.paifa_dingdan_shu
+ jinri_paifa_jine = float(today_stat.paifa_jine)
+ jinri_tuikuan_dingdan = today_stat.tuikuan_dingdan_shu
+ except ShangjiaRiTongji.DoesNotExist:
+ jinri_paifa_dingdan = 0
+ jinri_paifa_jine = 0.00
+ jinri_tuikuan_dingdan = 0
+
+ response_data['jinripaidan'] = jinri_paifa_dingdan # 今日派单数(新增)
+ response_data['jinrituikuan'] = jinri_tuikuan_dingdan # 今日退款数(新增)
+ response_data['riliushui'] = jinri_paifa_jine # 今日流水(派发总金额)
+
+ # ------------------- 4. 从 ShangjiaRiTongji 获取本月统计 -------------------
+ month_stats = ShangjiaRiTongji.objects.filter(
+ yonghuid=yonghuid,
+ riqi__gte=this_month_start,
+ riqi__lte=today
+ ).aggregate(
+ total_paifa_jine=Sum('paifa_jine'),
+ total_paifa_dingdan=Sum('paifa_dingdan_shu')
+ )
+ yue_paifa_jine = float(month_stats.get('total_paifa_jine') or 0.00)
+ # yue_paifa_dingdan = month_stats.get('total_paifa_dingdan') or 0 # 如果前端不需要可不加
+
+ response_data['yueliushui'] = yue_paifa_jine # 今月流水(派发总金额)
+
+ # (可选)如果前端还需要今月派单总数,取消下面注释
+ # response_data['jinyuepaidan'] = yue_paifa_dingdan
+
+ return Response({
+ 'code': 200,
+ 'msg': '获取商家信息成功',
+ 'data': response_data
+ }, status=status.HTTP_200_OK)
+
+
+
+
+
+
+
+
+
+
+
+
+class BossBiaoshiView(APIView):
+ """
+ 获取用户的GoEasy标识 - 极简实现
+ POST /yonghu/bossbiaoshi
+ 需要JWT Token认证
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 直接从request.user获取用户
+ user = request.user
+
+ # 2. 获取yonghuid(确保用户已登录)
+ if not user or not hasattr(user, 'yonghuid'):
+ return Response({
+ 'code': 401,
+ 'msg': '用户未认证或用户信息不完整'
+ }, status=401)
+
+ # 3. 生成标识:Boss + yonghuid(与前端约定)
+ boss_biaoshi = f"Boss{user.yonghuid}"
+
+ # 4. 返回给前端
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': {
+ 'boss_biaoshi': boss_biaoshi,
+ 'yonghuid': user.yonghuid,
+ 'user_type': user.user_type
+ }
+ })
+
+
+"""
+阿龙电竞陪玩平台 - 邀请码相关API接口
+"""
+
+class YaoqingmaView(APIView):
+ """
+ 获取或生成管事邀请码
+ 访问路径: /api/yonghu/yaoqingma/
+ 请求方法: GET
+ 权限要求: JWT Token认证通过的用户
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ """
+ 获取当前用户的邀请码
+ 逻辑:
+ 1. 通过JWT获取当前用户 (request.user)
+ 2. 反向查询UserGuanshi扩展表
+ 3. 验证管事状态(zhuangtai=1)
+ 4. 已有邀请码直接返回,没有则生成并保存
+ """
+ # 获取当前认证用户
+ current_user = request.user
+
+ # 1. 使用反向查询获取管事扩展信息 (性能最优方式)
+ try:
+ guanshi_profile = current_user.guanshi_profile
+ except UserGuanshi.DoesNotExist:
+ # 用户不是管事,没有扩展表记录
+ return Response({
+ 'code': 403,
+ 'message': '当前用户不是管事身份,无法生成邀请码',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 2. 验证管事状态 (zhuangtai 必须等于 1)
+ if guanshi_profile.zhuangtai != 1:
+ return Response({
+ 'code': 403,
+ 'message': '管事账号状态异常,无法生成邀请码',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 3. 检查是否已存在邀请码
+ existing_yaoqingma = guanshi_profile.yaoqingma
+
+ # 如果存在且非空,直接返回
+ if existing_yaoqingma and existing_yaoqingma.strip() != '':
+ return Response({
+ 'code': 200,
+ 'message': '成功获取现有邀请码',
+ 'data': {
+ 'yaoqingma': existing_yaoqingma,
+ 'is_new': False # 标记是否为新生成的
+ }
+ })
+
+ # 4. 生成新的邀请码 (使用用户ID字符串)
+ # 注意: 这里使用yonghuid,不是主键id,如你要求的"123456"这种形式
+ yonghuid_str = str(current_user.yonghuid)
+ new_yaoqingma = CreateInvitationCode(yonghuid_str)
+
+ # 5. 验证生成的邀请码格式
+ if not VerifyInvitationCode(new_yaoqingma):
+ return Response({
+ 'code': 500,
+ 'message': '邀请码生成失败,格式验证错误',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 6. 保存到数据库 (使用事务确保数据一致性)
+ try:
+ with transaction.atomic():
+ guanshi_profile.yaoqingma = new_yaoqingma
+ guanshi_profile.save()
+ except Exception as e:
+ # 这里可以记录日志
+ return Response({
+ 'code': 500,
+ 'message': f'保存邀请码到数据库失败: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 7. 返回新生成的邀请码
+ return Response({
+ 'code': 200,
+ 'message': '成功生成新的邀请码',
+ 'data': {
+ 'yaoqingma': new_yaoqingma,
+ 'is_new': True # 标记是新生成的
+ }
+ })
+
+
+
+"""
+阿龙电竞陪玩平台 - 打手注册及相关API接口
+"""
+
+
+class DashouZhuceView(APIView):
+ """
+ 打手注册接口 (通过管事邀请码)
+ 访问路径: /api/yonghu/dashouzhuce/
+ 请求方法: POST
+ 权限要求: JWT Token认证通过的用户
+ 请求体: { "inviteCode": "AbC123..." }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 获取并验证邀请码参数
+ yaoqingma = request.data.get('inviteCode')
+ if not yaoqingma:
+ return Response({
+ 'code': 400,
+ 'message': '邀请码不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ yaoqingma = yaoqingma.strip()
+ if len(yaoqingma) > 100:
+ return Response({
+ 'code': 400,
+ 'message': '邀请码长度超过限制',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ current_user = request.user
+ # 注意:这里request.user是UserMain实例
+
+ # 2. 检查用户是否已是打手 (使用反向查询,性能最优)
+ try:
+ dashou_profile = current_user.dashou_profile
+ # 用户已是打手,直接返回现有信息
+ return self.fanhuiXianyouDashouXinxi(dashou_profile, current_user)
+ except UserDashou.DoesNotExist:
+ # 用户不是打手,继续注册流程
+ pass
+
+ # 3. 根据邀请码查找对应的管事
+ try:
+ # 使用select_related一次性获取管事及其关联的主表用户信息,性能最高
+ guanshi_profile = UserGuanshi.objects.select_related('user').get(yaoqingma=yaoqingma)
+ except UserGuanshi.DoesNotExist:
+ return Response({
+ 'code': 404,
+ 'message': '邀请码无效或不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 验证管事状态
+
+ # 5. 核心:在数据库事务中创建用户的所有身份
+ try:
+ with transaction.atomic():
+ # 获取管事的用户ID (yonghuid)
+ guanshi_yonghuid = guanshi_profile.user.yonghuid
+
+ # 5.1 创建打手扩展表
+ dashou_profile = UserDashou.objects.create(
+ user=current_user,
+ nicheng='大手子', # 默认昵称
+ chenghao='普通大手', # 默认称号
+ yaoqingren=guanshi_yonghuid, # 设置邀请人ID
+ # 其他字段全部使用模型定义的默认值
+ )
+
+ # 5.2 创建商家扩展表
+ try:
+ boss_profile = current_user.boss_profile
+ if boss_profile.nickname:
+ boss_nickname = boss_profile.nickname
+ except:
+ pass
+
+
+ # 5.3 创建管事扩展表
+
+ # 5.4 更新主表的用户类型 (如果需要,可以设为'dashou',或保持原逻辑)
+ # current_user.user_type = 'dashou' # 根据你的业务逻辑决定
+ # current_user.save()
+
+ # 5.5 更新原管事的邀请数量 (使用行锁防止并发问题)
+ # 重新查询并锁定该行记录
+ locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
+ locked_guanshi.yaogingshuliang += 1
+ locked_guanshi.save()
+
+ # ========== 新增:管事每日统计(邀请打手) ==========
+ try:
+ update_guanshi_daily_by_action(
+ yonghuid=guanshi_yonghuid,
+ action=1, # 1 = 邀请打手
+ amount=Decimal('0.00')
+ )
+ logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}")
+ except Exception as e:
+ logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}")
+
+ except Exception as e:
+
+ # 事务会自动回滚
+ # 这里可以记录更详细的日志
+ return Response({
+ 'code': 500,
+ 'message': f'注册过程中发生系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 6. 注册成功,返回信息 (新注册用户没有会员记录,clumber返回空列表)
+ fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=True)
+ return Response({
+ 'code': 200,
+ 'message': '注册成功!已开通打手、管事身份。',
+ 'data': fanhui_data
+ })
+
+ def fanhuiXianyouDashouXinxi(self, dashou_profile, current_user):
+ """处理已是打手的用户,返回其现有信息"""
+ fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=False)
+ return Response({
+ 'code': 200,
+ 'message': '您已是打手,返回当前信息。',
+ 'data': fanhui_data
+ })
+
+ def zhuangbeiFanhuiShuju(self, dashou_profile, current_user, is_new=False):
+ """准备返回给前端的打手数据,字段名严格匹配前端"""
+
+
+
+ # 构建基础信息
+ data = {
+ 'dashounicheng': dashou_profile.nicheng,
+ 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
+ 'yongjin': str(dashou_profile.yue), # Decimal转字符串
+ 'zonge': str(dashou_profile.zonge),
+ 'yajin': str(dashou_profile.yajin),
+ 'chenghao': dashou_profile.chenghao,
+ 'jinfen': dashou_profile.jifen,
+ 'chengjiaoliang': dashou_profile.chengjiaozongliang,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'dashouzhuangtai': dashou_profile.zhuangtai,
+ }
+
+ # 查询并处理会员列表 (clumber)
+ # 使用当前用户的yonghuid进行查询
+ huiyuan_goumai_list = []
+ if not is_new: # 如果是新用户,直接返回空列表,无需查询
+ goumai_records = Huiyuangoumai.objects.filter(yonghu_id=current_user.yonghuid)
+ for record in goumai_records:
+ # 调用方法检查是否到期,此方法内部会更新状态
+ shifou_daoqi = record.jiance_shifou_daoqi()
+ huiyuan_goumai_list.append({
+ 'huiyuan_id': record.huiyuan_id,
+ 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
+ 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
+ 'shifou_daoqi': shifou_daoqi,
+ 'jieshao': record.jieshao
+ })
+
+ data['clumber'] = huiyuan_goumai_list
+ return data
+
+
+
+
+
+
+class GuanshiYaoqingDashouListView(APIView):
+ """
+ 管事获取已邀请打手列表(支持筛选、搜索、分页)
+ POST /api/yonghu/gshqyqds/
+ Headers: Authorization: Bearer
+ Body:
+ {
+ "page": 1, // 页码,从1开始
+ "pageSize": 30, // 每页条数,最大100
+ "keyword": "", // 搜索关键词(用户ID或昵称)
+ "zhanghaozhuangtai": 1, // 账号状态:1=正常,2=封禁(可选)
+ "zaixianzhuangtai": 1 // 在线状态:1=在线,2=离线(可选)
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ current_user = request.user
+ guanshi_yonghuid = current_user.yonghuid
+
+ # 1. 分页参数
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 30))
+ if page < 1 or page_size < 1:
+ return Response({'code': 400, 'message': '分页参数必须大于0'}, status=400)
+ if page_size > 100:
+ page_size = 100
+
+ # 2. 筛选参数
+ keyword = request.data.get('keyword', '').strip()
+ zhanghaozhuangtai = request.data.get('zhanghaozhuangtai') # 可选:1=正常,2=封禁
+ zaixianzhuangtai = request.data.get('zaixianzhuangtai') # 可选:1=在线,2=离线
+
+ # 3. 基础查询:当前管事邀请的所有打手
+ base_qs = UserDashou.objects.select_related('user').filter(
+ yaoqingren=guanshi_yonghuid
+ )
+
+ # 4. 全局统计(不受筛选影响,直接基于 base_qs)
+ total_all = base_qs.count()
+ zaixian_all = base_qs.filter(zaixianzhuangtai=1).count()
+ zhengchang_all = base_qs.filter(zhanghaozhuangtai=1).count()
+ fengjin_all = total_all - zhengchang_all
+
+ # 5. 应用筛选条件
+ if keyword:
+ base_qs = base_qs.filter(
+ Q(nicheng__icontains=keyword) | Q(user__yonghuid__icontains=keyword)
+ )
+ if zhanghaozhuangtai is not None:
+ try:
+ val = int(zhanghaozhuangtai)
+ if val == 1:
+ base_qs = base_qs.filter(zhanghaozhuangtai=1)
+ else:
+ base_qs = base_qs.exclude(zhanghaozhuangtai=1)
+ except (ValueError, TypeError):
+ pass
+ if zaixianzhuangtai is not None:
+ try:
+ val = int(zaixianzhuangtai)
+ if val == 1:
+ base_qs = base_qs.filter(zaixianzhuangtai=1)
+ else:
+ base_qs = base_qs.exclude(zaixianzhuangtai=1)
+ except (ValueError, TypeError):
+ pass
+
+ # 6. 排序和分页
+ base_qs = base_qs.order_by('-create_time')
+ paginator = Paginator(base_qs, page_size)
+ try:
+ page_obj = paginator.page(page)
+ except EmptyPage:
+ page_obj = []
+ has_more = False
+ filtered_total = paginator.count if paginator.count else 0
+ else:
+ has_more = page_obj.has_next()
+ filtered_total = paginator.count
+
+ # 7. 构建打手列表
+ dashou_list = []
+ for dashou in page_obj:
+ user_main = dashou.user
+ dashou_list.append({
+ 'uid': user_main.yonghuid,
+ 'nicheng': dashou.nicheng or '打手',
+ 'zaixianzhuangtai': dashou.zaixianzhuangtai,
+ 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
+ 'touxiang': user_main.avatar or '',
+ 'jiedanzongliang': dashou.jiedanzongliang,
+ 'chengjiaozongliang': dashou.chengjiaozongliang,
+ 'tuikuanliang': dashou.tuikuanliang,
+ 'yue': str(dashou.zonge),
+ })
+
+ # 8. 获取管事邀请总数(来自管事扩展表)
+ try:
+ yaoqingzongshu = current_user.guanshi_profile.yaogingshuliang
+ except Exception:
+ yaoqingzongshu = total_all
+
+ response_data = {
+ 'dashouList': dashou_list,
+ 'yaoqingzongshu': yaoqingzongshu, # 邀请总数(全局)
+ 'totalCount': filtered_total, # 当前筛选条件下的总数
+ 'zaixianCount': zaixian_all, # 全局在线
+ 'zhengchangCount': zhengchang_all, # 全局正常
+ 'fengjinCount': fengjin_all, # 全局封禁
+ 'currentPage': page,
+ 'pageSize': page_size,
+ 'hasMore': has_more,
+ }
+
+ return Response({'code': 200, 'message': '获取成功', 'data': response_data})
+
+ except ValueError:
+ return Response({'code': 400, 'message': '分页参数格式错误'}, status=400)
+ except Exception as e:
+ logger.error(f'获取打手列表失败: {str(e)}', exc_info=True)
+ return Response({'code': 500, 'message': '系统错误'}, status=500)
+
+
+
+
+
+class TixianXinxiHuoquView(APIView):
+ """
+ 获取用户提现收款信息接口
+ 返回用户已设置的手机号、收款码、收款账号
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ """
+ 处理获取收款信息的POST请求
+ 直接返回用户当前的收款设置
+ """
+ try:
+ # 从request.user获取当前用户
+ user_main = request.user
+
+ # 🔥【修改开始 - 使用Decimal,字段名是lilu】
+ # 查询打手提现费率 (fadanpingtai='5')
+ dashou_rate_obj = Lilubiao.objects.filter(fadanpingtai='5').first()
+ dashou_rate = decimal.Decimal(str(dashou_rate_obj.lilu)) if dashou_rate_obj else decimal.Decimal('0.00')
+
+ # 查询管事提现费率 (fadanpingtai='6')
+ guanshi_rate_obj = Lilubiao.objects.filter(fadanpingtai='6').first()
+ guanshi_rate = decimal.Decimal(str(guanshi_rate_obj.lilu)) if guanshi_rate_obj else decimal.Decimal('0.00')
+ # 🔥【修改结束】
+
+ # 构建返回数据
+ response_data = {
+ 'txdianhua': user_main.phone if user_main.phone else '',
+ 'txtupian': user_main.zhifu if user_main.zhifu else '',
+ 'txzh': user_main.skzhanghao if user_main.skzhanghao else '',
+ # 🔥【修改开始】
+ 'dashou_rate': dashou_rate, # 打手提现费率
+ 'guanshi_rate': guanshi_rate, # 管事提现费率
+ # 🔥【修改结束】
+ }
+
+ # 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ return Response({
+ 'code': 99,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+class ShoukuanXinxiShangchuanView(APIView):
+ """
+ 上传用户收款信息接口
+ 支持同时上传手机号、收款账号、收款码图片
+ 使用multipart/form-data格式
+ """
+ permission_classes = [permissions.IsAuthenticated]
+ parser_classes = (MultiPartParser,FormParser,JSONParser)
+
+ def post(self, request):
+ """
+ 处理上传收款信息的POST请求
+ 验证并保存用户的收款设置
+ """
+ try:
+ # 获取当前用户
+ user_main = request.user
+ yonghuid = user_main.yonghuid
+
+ # 准备响应数据
+ response_data = {}
+
+ # 1. 验证必填字段:手机号必须要有
+ txdianhua = request.data.get('txdianhua', '').strip()
+ if not txdianhua:
+ return Response({
+ 'code': 1,
+ 'msg': '收款人电话不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证手机号格式(11位数字)
+ if not txdianhua.isdigit() or len(txdianhua) != 11:
+ return Response({
+ 'code': 2,
+ 'msg': '请输入11位有效的手机号码',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 获取收款账号(可能为空)
+ txzh = request.data.get('txzh', '').strip()
+
+ # 3. 处理收款码图片上传(如果有)
+ txtupian_file = request.FILES.get('file') # 注意:前端upload.js使用'file'作为字段名
+ txtupian_url = ''
+
+ if txtupian_file:
+ # 验证图片
+ is_valid, error_msg = validate_image(txtupian_file)
+ if not is_valid:
+ return Response({
+ 'code': 3,
+ 'msg': f'图片验证失败: {error_msg}',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 上传收款码图片
+ txtupian_url = self.handle_shoukuanma_upload(user_main, txtupian_file)
+ if txtupian_url:
+ response_data['txtupian'] = txtupian_url
+ else:
+ return Response({
+ 'code': 4,
+ 'msg': '收款码上传失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+ else:
+ response_data['txtupian'] = ''
+
+ # 4. 验证收款账号和收款码至少有一个
+ if not txzh and not txtupian_url:
+ return Response({
+ 'code': 5,
+ 'msg': '请填写收款账号或上传收款码',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 保存所有信息到数据库
+ with transaction.atomic():
+ user_main.phone = txdianhua
+ user_main.skzhanghao = txzh if txzh else ''
+
+ # 只有上传了图片才更新zhifu字段
+ if txtupian_url:
+ user_main.zhifu = txtupian_url
+
+ user_main.save()
+
+ # 6. 构建完整的返回数据
+ response_data['txdianhua'] = txdianhua
+ response_data['txzh'] = txzh if txzh else ''
+
+ # 7. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '收款信息设置成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ traceback.print_exc() # 打印详细错误信息到控制台
+ return Response({
+ 'code': 99,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def handle_shoukuanma_upload(self, user_main, image_file):
+ """
+ 处理收款码图片上传到腾讯云COS
+ 存储路径: tixian/{yonghuid}/{yonghuid}.扩展名
+ """
+ try:
+ yonghuid = user_main.yonghuid
+ if not yonghuid:
+ return None
+
+ # 获取旧收款码URL
+ old_shoukuanma_url = user_main.zhifu
+
+ # 生成文件扩展名
+ file_name = image_file.name
+ file_extension = os.path.splitext(file_name)[1].lower()
+ if not file_extension:
+ file_extension = '.jpg'
+
+ # 构建OSS文件路径: tixian/{yonghuid}/{yonghuid}{扩展名}
+ # 使用用户ID作为文件名,确保同一用户多次上传会覆盖
+ oss_file_name = f"{yonghuid}{file_extension}"
+ oss_file_path = f"tixian/{yonghuid}/{oss_file_name}"
+
+ # 删除旧收款码(如果存在)
+ if old_shoukuanma_url and old_shoukuanma_url.strip():
+ delete_from_oss(old_shoukuanma_url)
+
+ # 上传新收款码到OSS
+ new_shoukuanma_url = upload_to_oss(image_file, oss_file_path)
+
+ if new_shoukuanma_url:
+ # 返回相对路径(从tixian文件夹开始)
+ return oss_file_path
+ else:
+ return None
+
+ except Exception as e:
+ print(f"处理收款码上传失败: {e}")
+ return None
+
+
+
+
+
+
+# ========== 订单状态常量(请根据您的实际业务修改)==========
+# 假设状态值:已完成=9,已退款=10(示例,实际以您的项目为准)
+ORDER_STATUS_COMPLETED = 3 # 订单已完成
+ORDER_STATUS_REFUNDED = 5 # 订单已退款
+ORDER_STATUS_FAIL = 6
+# 进行中状态(不可提现)例如:待接单、已接单、进行中等,您需要定义排除列表
+FORBIDDEN_STATUS_FOR_WITHDRAW = [1, 2, 3, 4, 5, 6, 7, 8] # 除已完成和已退款外的所有状态
+
+
+class TixianShenqingView(APIView):
+ """
+ 用户提现申请接口(完整版)
+ 支持类型:
+ 1-打手佣金提现
+ 2-管事分红提现
+ 3-组长分红提现
+ 4-审核官分佣提现
+ 5-打手押金提现(退出平台)
+ 6-商家余额提现(退出平台)
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ user_main = request.user
+ yonghuid = user_main.yonghuid
+
+ # 1. 获取并验证参数
+ leixing = request.data.get('leixing')
+ jine = request.data.get('jine')
+ fangshi = request.data.get('fangshi')
+ txskfs = request.data.get('txskfs', 0)
+
+ # 罚单检查(共用)
+ fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2)
+ if not fadan_ok:
+ return Response({'code': 400, 'msg': fadan_msg})
+
+ # 参数完整性
+ missing = []
+ if leixing is None: missing.append('leixing')
+ if jine is None: missing.append('jine')
+ if fangshi is None: missing.append('fangshi')
+ if missing:
+ return Response({'code': 1, 'msg': f'缺少参数: {", ".join(missing)}'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ leixing = int(leixing)
+ jine = decimal.Decimal(str(jine))
+ fangshi = int(fangshi)
+ txskfs = int(txskfs)
+ except (ValueError, TypeError, decimal.InvalidOperation) as e:
+ return Response({'code': 2, 'msg': f'参数格式错误: {str(e)}'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if jine <= decimal.Decimal('0'):
+ return Response({'code': 3, 'msg': '提现金额必须大于0'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # 支持的提现类型(1-6)
+ if leixing not in [1, 2, 3, 4, 5, 6]:
+ return Response({'code': 4, 'msg': '无效的提现类型'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if fangshi not in [1, 2]:
+ return Response({'code': 5, 'msg': '无效的收款方式,只能是1(微信)或2(支付宝)'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 获取对应费率及手续费
+ current_rate = decimal.Decimal('0.00')
+ rate_key = {
+ 1: '5', # 打手佣金
+ 2: '6', # 管事分红
+ 3: '8', # 组长分红
+ 4: '9', # 审核官分佣
+ 5: '11', # 打手押金提现
+ 6: '10', # 商家余额提现
+ }.get(leixing)
+
+ if rate_key:
+ rate_obj = Lilubiao.objects.filter(fadanpingtai=rate_key).first()
+ if rate_obj:
+ current_rate = decimal.Decimal(str(rate_obj.lilu))
+ else:
+ return Response({'code': 41, 'msg': f'提现费率未配置(类型{leixing})'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ shouxufei = jine * current_rate
+ shijidaozhang = jine - shouxufei
+ if shijidaozhang <= decimal.Decimal('0'):
+ return Response({'code': 6, 'msg': '提现金额过低,扣除手续费后无实际到账'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 根据提现类型执行校验和扣款
+ nicheng = ''
+ if leixing == 1:
+ result = self._check_dashou_commission_withdraw(user_main, jine)
+ if result['code'] != 0:
+ return Response(result, status=status.HTTP_400_BAD_REQUEST)
+ nicheng = result['data']['nicheng']
+ # 扣款(佣金)
+ with transaction.atomic():
+ profile = UserDashou.objects.select_for_update().get(user=user_main)
+ if profile.yue < jine:
+ return Response({'code': 13, 'msg': '余额不足'})
+ profile.yue = F('yue') - jine
+ profile.save()
+ elif leixing == 2:
+ result = self._check_guanshi_dividend_withdraw(user_main, jine)
+ if result['code'] != 0:
+ return Response(result, status=status.HTTP_400_BAD_REQUEST)
+ with transaction.atomic():
+ profile = UserGuanshi.objects.select_for_update().get(user=user_main)
+ if profile.yue < jine:
+ return Response({'code': 22, 'msg': '余额不足'})
+ profile.yue = F('yue') - jine
+ profile.save()
+ nicheng = "管事用户"
+ elif leixing == 3:
+ result = self._check_zuzhang_withdraw(user_main, jine)
+ if result['code'] != 0:
+ return Response(result, status=status.HTTP_400_BAD_REQUEST)
+ with transaction.atomic():
+ profile = UserZuzhang.objects.select_for_update().get(user=user_main)
+ if profile.ketixian_jine < jine:
+ return Response({'code': 32, 'msg': '可提现金额不足'})
+ profile.ketixian_jine = F('ketixian_jine') - jine
+ profile.save()
+ nicheng = user_main.nicheng if hasattr(user_main, 'nicheng') else ''
+ elif leixing == 4:
+ result = self._check_shenheguan_withdraw(user_main, jine)
+ if result['code'] != 0:
+ return Response(result, status=status.HTTP_400_BAD_REQUEST)
+ with transaction.atomic():
+ profile = UserShenheguan.objects.select_for_update().get(user=user_main)
+ if profile.yue < jine:
+ return Response({'code': 42, 'msg': '可提现金额不足'})
+ profile.yue = F('yue') - jine
+ profile.save()
+ nicheng = "审核官"
+ elif leixing == 5:
+ # 打手押金提现(退出)
+ result = self._check_dashou_yajin_withdraw(user_main, jine)
+ if result['code'] != 0:
+ return Response(result, status=status.HTTP_400_BAD_REQUEST)
+ with transaction.atomic():
+ profile = UserDashou.objects.select_for_update().get(user=user_main)
+ if profile.yajin < jine:
+ return Response({'code': 53, 'msg': '押金余额不足'})
+ profile.yajin = F('yajin') - jine
+ # 押金提现后封禁打手账号
+ profile.zhanghaozhuangtai = 0 # 0表示禁用
+ profile.save()
+ nicheng = profile.nicheng or ''
+ elif leixing == 6:
+ # 商家余额提现(退出)
+ result = self._check_shangjia_withdraw(user_main, jine)
+ if result['code'] != 0:
+ return Response(result, status=status.HTTP_400_BAD_REQUEST)
+ with transaction.atomic():
+ profile = UserShangjia.objects.select_for_update().get(user=user_main)
+ if profile.yue < jine:
+ return Response({'code': 63, 'msg': '商家余额不足'})
+ profile.yue = F('yue') - jine
+ # 注意:是否封禁商家账号?根据需求,用户未明确要求封禁,暂时只扣款,可后续扩展。
+ profile.save()
+ nicheng = profile.nicheng or ''
+
+ # 4. 创建提现记录(先扣款后创建,确保资金安全)
+ try:
+ avatar = user_main.avatar or ''
+ phone = user_main.phone or ''
+ zhifu = user_main.zhifu or ''
+ skzhanghao = user_main.skzhanghao or ''
+ tixian_record = Tixianjilu.objects.create(
+ yonghuid=yonghuid,
+ avatar=avatar,
+ phone=phone,
+ nicheng=nicheng,
+ leixing=leixing,
+ zhifu=zhifu,
+ skzhanghao=skzhanghao,
+ jine=shijidaozhang, # 记录实际到账金额
+ zhuangtai=1, # 审核中
+ fangshi=fangshi,
+ shenheid=None,
+ bhliyou='',
+ )
+ except Exception as e:
+ return Response({'code': 99, 'msg': f'创建提现记录失败: {str(e)}'},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 5. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '提现申请提交成功',
+ 'data': {
+ 'tixian_id': tixian_record.id,
+ 'leixing': leixing,
+ 'jine': str(jine.quantize(decimal.Decimal('0.01'))),
+ 'fangshi': fangshi,
+ 'zhuangtai': 1,
+ 'create_time': tixian_record.create_time.strftime('%Y-%m-%d %H:%M:%S'),
+ 'current_rate': str(current_rate.quantize(decimal.Decimal('0.0000'))),
+ 'shouxufei': str(shouxufei.quantize(decimal.Decimal('0.01'))),
+ 'shijidaozhang': str(shijidaozhang.quantize(decimal.Decimal('0.01'))),
+ 'tip': f'本次提现手续费率{current_rate * 100}%,手续费{shouxufei}元,实际到账{shijidaozhang}元'
+ }
+ })
+
+ except Exception as e:
+ traceback.print_exc()
+ return Response({'code': 99, 'msg': f'系统错误: {str(e)}'},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # ==================== 校验函数 ====================
+
+ def _check_dashou_commission_withdraw(self, user_main, jine):
+ """打手佣金提现校验"""
+ try:
+ dashou = UserDashou.objects.get(user=user_main)
+ except ObjectDoesNotExist:
+ return {'code': 10, 'msg': '您不是打手身份,无法提现佣金'}
+
+ if dashou.zhanghaozhuangtai != 1:
+ return {'code': 11, 'msg': '打手账号已被封禁,无法提现'}
+ if dashou.zhuangtai != 1:
+ return {'code': 12, 'msg': '您有订单进行中,请完成后提现'}
+ if dashou.jifen != 10:
+ return {'code': 15, 'msg': '积分不足10分,请补充积分后提现'}
+ if dashou.yue < jine:
+ return {'code': 13, 'msg': f'余额不足,当前余额: {dashou.yue}'}
+
+ return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
+
+ def _check_guanshi_dividend_withdraw(self, user_main, jine):
+ """管事分红提现校验"""
+ try:
+ guanshi = UserGuanshi.objects.get(user=user_main)
+ except ObjectDoesNotExist:
+ return {'code': 20, 'msg': '您不是管事身份,无法提现分红'}
+ if guanshi.zhuangtai != 1:
+ return {'code': 21, 'msg': '管事账号已被封禁,无法提现'}
+ if guanshi.yue < jine:
+ return {'code': 22, 'msg': f'余额不足,当前余额: {guanshi.yue}'}
+ return {'code': 0, 'msg': 'ok', 'data': {}}
+
+ def _check_zuzhang_withdraw(self, user_main, jine):
+ """组长分红提现校验"""
+ try:
+ zuzhang = UserZuzhang.objects.get(user=user_main)
+ except ObjectDoesNotExist:
+ return {'code': 30, 'msg': '您不是组长身份,无法提现组长分红'}
+ if zuzhang.zhuangtai != 1:
+ return {'code': 31, 'msg': '组长账号已被封禁,无法提现'}
+ if zuzhang.ketixian_jine < jine:
+ return {'code': 32, 'msg': f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}'}
+ return {'code': 0, 'msg': 'ok', 'data': {}}
+
+ def _check_shenheguan_withdraw(self, user_main, jine):
+ """审核官分佣提现校验"""
+ try:
+ shenheguan = UserShenheguan.objects.get(user=user_main)
+ except ObjectDoesNotExist:
+ return {'code': 40, 'msg': '您不是审核官身份,无法提现分佣'}
+ if shenheguan.zhuangtai != 1:
+ return {'code': 41, 'msg': '审核官账号已被封禁,无法提现'}
+ if shenheguan.yue < jine:
+ return {'code': 42, 'msg': f'可提现金额不足,当前可提: {shenheguan.yue}'}
+ return {'code': 0, 'msg': 'ok', 'data': {}}
+
+
+ def _check_shangjia_withdraw(self, user_main, jine):
+ """
+ 商家余额提现校验(退出平台)
+ 包含:订单、自身罚单、申请的罚单检查
+ """
+ try:
+ shangjia = UserShangjia.objects.get(user=user_main)
+ except ObjectDoesNotExist:
+ return {'code': 60, 'msg': '您不是商家身份,无法提现商家余额'}
+
+ if shangjia.zhuangtai != 1:
+ return {'code': 61, 'msg': '商家账号已被禁用,无法提现'}
+ if shangjia.yue < jine:
+ return {'code': 62, 'msg': f'商家余额不足,当前余额: {shangjia.yue}'}
+
+ # 订单检查(使用 Q 取反)
+ unfinished_orders = DingdanShangjia.objects.filter(
+ shangjia_id=user_main.yonghuid,
+ ).exclude(
+ dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED,ORDER_STATUS_FAIL]
+ ).exists()
+ if unfinished_orders:
+ return {'code': 63, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提现余额'}
+
+ # 自身被处罚的罚单检查(状态不是已缴纳(2)和已驳回(4))
+ self_fadan = Fadan.objects.filter(
+ beichufa_id=user_main.yonghuid,
+ ).exclude(
+ zhuangtai__in=[2, 4]
+ ).exists()
+ if self_fadan:
+ return {'code': 64, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'}
+
+ # 申请的对他人处罚罚单检查(状态不是已缴纳(2)和已驳回(4))
+ applied_fadan = Fadan.objects.filter(
+ shenqing_chufa=user_main.yonghuid,
+ ).exclude(
+ zhuangtai__in=[2, 4]
+ ).exists()
+ if applied_fadan:
+ return {'code': 65, 'msg': '您发起的对其他用户的处罚尚未处理完毕(待缴纳/申诉中),请等待完结'}
+
+ return {'code': 0, 'msg': 'ok', 'data': {'nicheng': shangjia.nicheng or ''}}
+
+ def _check_dashou_yajin_withdraw(self, user_main, jine):
+ """
+ 打手押金提现校验(退出平台)
+ 要求:无进行中的订单,无未结算订单,自身罚单需已处理
+ """
+ try:
+ dashou = UserDashou.objects.get(user=user_main)
+ except ObjectDoesNotExist:
+ return {'code': 50, 'msg': '您不是打手身份,无法提取押金'}
+
+ if dashou.zhanghaozhuangtai != 1:
+ return {'code': 51, 'msg': '打手账号已禁用,无法提取押金'}
+ if dashou.zhuangtai != 1:
+ return {'code': 52, 'msg': '您有订单进行中,请完成后提取押金'}
+ if dashou.yajin < jine:
+ return {'code': 53, 'msg': f'押金余额不足,当前押金: {dashou.yajin}'}
+
+ # 订单检查(使用 exclude + __in)
+ unfinished_orders = Dingdan.objects.filter(
+ jiedan_dashou_id=user_main.yonghuid,
+ ).exclude(
+ zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]
+ ).exists()
+ if unfinished_orders:
+ return {'code': 54, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提取押金'}
+
+ # 自身罚单检查
+ self_fadan = Fadan.objects.filter(
+ beichufa_id=user_main.yonghuid,
+ ).exclude(
+ zhuangtai__in=[2, 4]
+ ).exists()
+ if self_fadan:
+ return {'code': 55, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'}
+
+ return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
+
+
+
+
+
+
+
+
+
+class TixianJiluHuoquViewV2(APIView):
+ """
+ 提现记录获取接口
+ mode=1:手动提现记录(默认,兼容旧版)
+ mode=2:自动提现审核记录(含 shenhe_jilu_id / shenhe_danhao,支持 zhuangtai 筛选)
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ try:
+ user_main = request.user
+ yonghuid = user_main.yonghuid
+ page = request.data.get('page', 1)
+ limit = request.data.get('limit', 50)
+ mode = int(request.data.get('mode', 1) or 1)
+ zhuangtai_filter = int(request.data.get('zhuangtai', 0) or 0)
+
+ try:
+ page = int(page)
+ limit = int(limit)
+ if page < 1 or limit < 1:
+ return Response({'code': 1, 'msg': '参数错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+ except (TypeError, ValueError):
+ return Response({'code': 1, 'msg': '参数格式错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if limit > 100:
+ limit = 100
+ offset = (page - 1) * limit
+
+ qs = Tixianjilu.objects.filter(yonghuid=yonghuid)
+ if mode == 2:
+ # 自动提现:含新审核关联记录;zhuangtai>0 时按状态筛选
+ if zhuangtai_filter > 0:
+ qs = qs.filter(zhuangtai=zhuangtai_filter)
+ qs = qs.order_by('-create_time')
+
+ records_queryset = qs.values(
+ 'id', 'leixing', 'jine', 'zhuangtai', 'create_time', 'fangshi',
+ 'phone', 'skzhanghao', 'zhifu', 'bhliyou',
+ 'shenhe_jilu_id', 'shenhe_danhao',
+ )[offset:offset + limit + 1]
+
+ records_list = list(records_queryset)
+ has_more = len(records_list) > limit
+ records_to_return = records_list[:limit] if has_more else records_list
+
+ # 批量取审核表 fail_reason
+ audit_ids = [r['shenhe_jilu_id'] for r in records_to_return if r.get('shenhe_jilu_id')]
+ audit_map = {}
+ if audit_ids:
+ for a in TixianShenheJilu.objects.filter(id__in=audit_ids).values(
+ 'id', 'fail_reason', 'shenqing_jine', 'shenhe_danhao'
+ ):
+ audit_map[a['id']] = a
+
+ # 批量取自动打款失败原因
+ danhao_list = [r['shenhe_danhao'] for r in records_to_return if r.get('shenhe_danhao')]
+ auto_fail_map = {}
+ if danhao_list:
+ for ar in TixianAutoRecord.objects.filter(
+ shenhe_danhao__in=danhao_list, zhuangtai=2
+ ).order_by('-create_time').values('shenhe_danhao', 'fail_reason'):
+ if ar['shenhe_danhao'] not in auto_fail_map:
+ auto_fail_map[ar['shenhe_danhao']] = ar['fail_reason']
+
+ formatted_records = []
+ for record in records_to_return:
+ audit = audit_map.get(record.get('shenhe_jilu_id')) if record.get('shenhe_jilu_id') else None
+ fail_reason = ''
+ if audit and audit.get('fail_reason'):
+ fail_reason = audit['fail_reason']
+ elif record.get('shenhe_danhao'):
+ fail_reason = auto_fail_map.get(record['shenhe_danhao'], '') or ''
+
+ jine_val = float(record['jine']) if record['jine'] else 0.00
+ if mode == 2 and audit and audit.get('shenqing_jine') is not None:
+ jine_val = float(audit['shenqing_jine'])
+
+ shenhe_danhao_val = record.get('shenhe_danhao') or (audit.get('shenhe_danhao') if audit else '')
+ formatted_records.append({
+ 'id': record['id'],
+ 'tixianjilu_id': record['id'],
+ 'tixian_id': record['id'],
+ 'leixing': record['leixing'],
+ 'jine': jine_val,
+ 'zhuangtai': record['zhuangtai'],
+ 'create': record['create_time'].strftime('%Y-%m-%d %H:%M:%S'),
+ 'fangshi': record['fangshi'],
+ 'txdianhua': record['phone'] or '',
+ 'txzh': record['skzhanghao'] or '',
+ 'txtupian': record['zhifu'] or '',
+ 'shibaiyuanyin': record['bhliyou'] or '',
+ 'bhliyou': record['bhliyou'] or '',
+ 'fail_reason': fail_reason,
+ 'shenhe_jilu_id': record.get('shenhe_jilu_id'),
+ 'shenhe_danhao': shenhe_danhao_val,
+ })
+
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': {
+ 'list': formatted_records,
+ 'has_more': has_more,
+ 'page': page,
+ 'limit': limit,
+ 'current_count': len(formatted_records),
+ },
+ })
+
+ except Exception as e:
+ return Response({
+ 'code': 99,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None,
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+# yonghu/views.py
+class AdminLoginView(APIView):
+ """
+ 管理员登录接口
+ 路径:/yonghu/adlog/
+ 方法:POST
+ 字段:zhanghao(账号), mima(密码), ip(二次验证码)
+ """
+
+ # 限制匿名用户访问频率,防止暴力破解
+ throttle_classes = [AnonRateThrottle]
+ # 允许任何人访问(登录接口不需要认证)
+ permission_classes = [AllowAny]
+
+ def post(self, request):
+ """
+ 处理管理员登录请求
+ 安全策略:任何错误都返回401,不透露具体信息
+ """
+ try:
+ # 1. 获取客户端真实IP和用户代理信息
+ kehuduan_ip = huoquKehuduanIP(request)
+ yonghu_daili = huoquYonghuDaili(request)
+
+
+ # 2. 获取前端传递的数据
+ data = request.data
+
+ # 检查必需字段是否存在
+ bixu_ziduan = ['zhanghao', 'mima', 'ip']
+ for ziduan in bixu_ziduan:
+ if ziduan not in data:
+ # 字段缺失,返回401
+ return Response(
+ {'code': 401, 'msg': '登录失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 提取并清理数据
+ zhanghao = data.get('zhanghao', '').strip()
+ mima = data.get('mima', '').strip()
+ erci_yanzhengma = data.get('ip', '').strip() # 二次验证码(前端叫ip)
+
+ # 简单验证字段非空
+ if not zhanghao or not mima or not erci_yanzhengma:
+ return Response(
+ {'code': 401, 'msg': '登录失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 3. 使用事务确保数据一致性
+ with transaction.atomic():
+ # 【优化点1】使用select_related一次性获取管理员扩展表
+ # 只查询管理员类型的用户
+ user = UserMain.objects.filter(
+ phone=zhanghao, # 手机号作为账号
+ password=mima, # 密码
+ user_type='admin' # 必须是管理员类型
+ ).select_related('admin_profile').first()
+
+ # 如果用户不存在或不是管理员,返回401
+ if not user:
+ return Response(
+ {'code': 401, 'msg': '登录失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 【优化点2】检查管理员扩展表是否存在
+ if not hasattr(user, 'admin_profile'):
+ return Response(
+ {'code': 401, 'msg': '登录失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 【优化点3】验证二次密码(管理员扩展表中的password字段)
+ admin_profile = user.admin_profile
+ if admin_profile.password != erci_yanzhengma:
+ return Response(
+ {'code': 401, 'msg': '登录失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 更新用户登录信息
+ # 获取当前时间
+ now_time = timezone.now()
+
+ # 处理IP地址长度问题
+ # 如果原ip字段长度不够,截取前11个字符(保持兼容性)
+
+ cunchu_ip = kehuduan_ip
+ #print(cunchu_ip)
+
+ # 更新用户信息
+ user.ip = cunchu_ip # 存储客户端IP
+ user.last_login_time = now_time # 更新最后登录时间
+ user.save()
+
+ # 【优化点4】生成JWT token(使用和微信登录相同的方式)
+ # 使用djangorestframework-simplejwt生成token
+ refresh = RefreshToken.for_user(user)
+ token = str(refresh.access_token)
+
+ # 5. 准备返回数据
+ # 返回用户基本信息(可根据需要调整)
+ response_data = {
+ 'token': token,
+ 'yonghuid': user.yonghuid,
+ 'phone': user.phone or '',
+ 'user_type': user.user_type,
+ 'last_login_time': now_time.strftime('%Y-%m-%d %H:%M:%S') if now_time else '',
+ }
+
+ # 可以添加管理员特定信息
+ if hasattr(user, 'admin_profile'):
+ response_data['wechat'] = user.admin_profile.wechat or ''
+
+ return Response({
+ 'code': 0,
+ 'msg': '登录成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ # 【安全点】任何异常都返回401,不暴露具体错误信息
+ # 记录日志用于排查问题,但前端只收到通用错误信息
+ # 这里可以添加日志记录,比如:
+ logging.error(f'管理员登录异常: {str(e)}', exc_info=True)
+
+ return Response(
+ {'code': 401, 'msg': '登录失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+
+# yonghu/views.py
+class AdminFinancialDataView(APIView):
+ """
+ 管理员财务数据接口
+ 路径:/yonghu/adhq/
+ 方法:POST
+ 认证:JWT Token
+ 权限:仅管理员
+ 返回:收支记录数据
+ """
+
+ # 使用JWT认证
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ """
+ 处理管理员财务数据请求
+ 安全策略:任何错误都返回401,不透露具体信息
+ """
+ try:
+ # 1. 获取前端传递的数据
+ data = request.data
+
+ # 检查必需字段
+ if 'zhanghao' not in data:
+ return Response(
+ {'code': 401, 'msg': '请求参数错误', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ qianzhanghao = data.get('zhanghao', '').strip()
+
+ # 简单验证字段非空
+ if not qianzhanghao:
+ return Response(
+ {'code': 401, 'msg': '请求参数错误', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 2. 获取当前登录用户(JWT认证后,request.user就是UserMain实例)
+ dangqianyonghu = request.user
+
+ # 3. 验证用户类型是否为admin
+ if not hasattr(dangqianyonghu, 'user_type') or dangqianyonghu.user_type != 'admin':
+ return Response(
+ {'code': 401, 'msg': '权限不足', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证前端传递的账号与当前用户的phone字段是否一致
+ if not dangqianyonghu.phone or dangqianyonghu.phone != qianzhanghao:
+ return Response(
+ {'code': 401, 'msg': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在(反向查询)
+ # 使用高效的ORM查询,避免额外的数据库查询
+ try:
+ # 使用select_related一次性获取管理员扩展表
+ # 注意:这里假设user_type='admin'的用户都有admin_profile
+ yonghu_xiangxi = UserMain.objects.select_related('admin_profile').get(
+ pk=dangqianyonghu.pk,
+ user_type='admin'
+ )
+
+ # 检查管理员扩展表是否存在
+ if not hasattr(yonghu_xiangxi, 'admin_profile'):
+ return Response(
+ {'code': 401, 'msg': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ except UserMain.DoesNotExist:
+ return Response(
+ {'code': 401, 'msg': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 查询收支记录表(Szjilu)
+ # 使用高效的ORM查询,获取最新的一条记录
+ # 注意:根据你的业务需求,这里获取最新的一条记录
+ # 如果需要汇总多条记录,可以使用aggregate进行聚合查询
+
+ # 方法1:获取最新的一条记录(假设表中只有一条汇总记录)
+ shouzhijilu = Szjilu.objects.order_by('-create_time').first()
+
+ # 方法2:如果表中可能有多条记录,需要汇总,可以使用以下方式:
+ # from django.db.models Sum
+ # zonghe = Szjilu.objects.aggregate(
+ # zongliushui=Sum('zongls'),
+ # zongshouyi=Sum('zongsy'),
+ # zongzhichu=Sum('zongzc'),
+ # jrls=Sum('jrls'),
+ # jrzc=Sum('jrzc')
+ # )
+
+ # 准备返回数据
+ if shouzhijilu:
+ # 有记录的情况
+ huizongshuju = {
+ 'zongliushui': float(shouzhijilu.zongls or 0),
+ 'zongshouyi': float(shouzhijilu.zongsy or 0),
+ 'zongzhichu': float(shouzhijilu.zongzc or 0),
+ 'jrls': float(shouzhijilu.jrls or 0),
+ 'jrzc': float(shouzhijilu.jrzc or 0)
+ }
+ else:
+ # 没有记录的情况,返回默认值
+ huizongshuju = {
+ 'zongliushui': 0.00,
+ 'zongshouyi': 0.00,
+ 'zongzhichu': 0.00,
+ 'jrls': 0.00,
+ 'jrzc': 0.00
+ }
+
+ # 7. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': huizongshuju
+ })
+
+ except Exception as e:
+ # 【安全策略】任何异常都返回401,不暴露具体错误信息
+ # 这里可以记录日志,但不返回给前端
+ # logging.error(f'管理员财务数据接口异常: {str(e)}', exc_info=True)
+
+ return Response(
+ {'code': 401, 'msg': '获取失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+
+
+
+
+
+# shangpin/views.py
+class AdGetOrderTypes(APIView):
+ """
+ 后台获取订单类型列表接口
+ 前端调用地址: /shangpin/getOrderTypes
+ 前端需要字段: id, biaoti, tupian_url
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+
+ if not qianzhanghao:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. 身份验证(按你说的方式)
+ dangqianyonghu = request.user
+
+ # 验证手机号
+ if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证管理员身份
+ if dangqianyonghu.user_type != 'admin':
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 3. 验证管理员扩展表
+ try:
+ guanliyuan = dangqianyonghu.admin_profile
+ except AttributeError:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 4. 查询订单类型(商品类型)表
+ # 注意:这里需要根据你实际的模型名调整
+ # 如果模型名不是ShangpinType,请告诉我正确的模型名
+
+ # 假设模型名是ShangpinLeixing(因为我之前看到你有商品类型表)
+ # 你需要告诉我确切的模型名
+ try:
+ # 请替换为你的实际模型名
+ # 只查询前端需要的三个字段
+ shangpin_types = ShangpinLeixing.objects.all().only('id', 'jieshao', 'tupian_url')
+
+ # 构建响应数据
+ type_list = [
+ {
+ 'id': item.id,
+ 'biaoti': item.jieshao or '',
+ 'tupian_url': item.tupian_url or ''
+ }
+ for item in shangpin_types
+ ]
+
+
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': type_list
+ })
+
+ except Exception as e:
+ logger.error(f"查询商品类型表失败: {str(e)}")
+ # 如果表不存在,返回空数组
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': []
+ })
+
+ except Exception as e:
+ logger.error(f"获取订单类型异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+
+
+
+# yonghu/views.py 或 dingdan/views.py
+class AdGetDingdanList(APIView):
+ """
+ 后台订单列表获取接口
+ 前端调用地址: /yonghu/adddhq
+ 前端需要字段: dingdan_id, jieshao, zhuangtai, leixing, fadanpingtai, jiage, tupian_url, fadanshijian
+ 统计字段: stats.platform.total, stats.platform.completed, stats.merchant.total, stats.merchant.completed
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取参数(严格按前端传的字段名)
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 30))
+
+ # 搜索相关参数
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+
+ # 筛选相关参数(严格按前端字段名)
+ fadanpingtai = request.data.get('fadanpingtai') # 前端叫 fadanpingtai
+ leixing = request.data.get('leixing') # 前端叫 leixing
+ zhuangtai = request.data.get('zhuangtai', '') # 前端叫 zhuangtai
+
+ if not qianzhanghao:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. 身份验证(与你说的完全一样)
+ dangqianyonghu = request.user
+
+ # 验证手机号
+ if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证管理员身份
+ if dangqianyonghu.user_type != 'admin':
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 3. 验证管理员扩展表
+ try:
+ guanliyuan = dangqianyonghu.admin_profile
+ except AttributeError:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 5. 构建查询条件(严格按前端字段名映射到数据库字段)
+ query_conditions = Q()
+
+ # 判断是否为搜索模式
+ is_search_mode = bool(dingdan_id)
+
+ if is_search_mode:
+ # 搜索模式:只按订单ID查询
+ query_conditions &= Q(dingdan_id=dingdan_id)
+ else:
+ # 筛选模式
+
+ # 发单平台筛选(前端: fadanpingtai -> 数据库: fadan_pingtai)
+ if fadanpingtai is not None:
+ try:
+ fadanpingtai_int = int(fadanpingtai)
+ if fadanpingtai_int in [1, 2]:
+ query_conditions &= Q(fadan_pingtai=fadanpingtai_int)
+ except (ValueError, TypeError):
+ pass
+
+ # 订单类型筛选(前端: leixing -> 数据库: leixing_id)
+ if leixing is not None:
+ try:
+ leixing_int = int(leixing)
+ query_conditions &= Q(leixing_id=leixing_int)
+ except (ValueError, TypeError):
+ pass
+
+ # 订单状态筛选(前端: zhuangtai -> 数据库: zhuangtai)
+ if zhuangtai and zhuangtai != 'all':
+ try:
+ # 处理多个状态,如 "1,7"
+ status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
+ if status_list:
+ query_conditions &= Q(zhuangtai__in=status_list)
+ except (ValueError, TypeError):
+ pass
+
+ # 6. 获取统计数据(平台订单和商家订单)
+ # 注意:这里统计所有订单,不考虑筛选条件
+ try:
+ # 平台订单统计 (fadan_pingtai=1)
+ platform_total = Dingdan.objects.filter(fadan_pingtai=1).count()
+ platform_completed = Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=3).count()
+
+ # 商家订单统计 (fadan_pingtai=2)
+ merchant_total = Dingdan.objects.filter(fadan_pingtai=2).count()
+ merchant_completed = Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=3).count()
+
+ platform_stats = {
+ 'total': platform_total,
+ 'completed': platform_completed
+ }
+ merchant_stats = {
+ 'total': merchant_total,
+ 'completed': merchant_completed
+ }
+ except Exception as e:
+ logger.warning(f"统计数据查询失败: {str(e)}")
+ platform_stats = {'total': 0, 'completed': 0}
+ merchant_stats = {'total': 0, 'completed': 0}
+
+ # 7. 执行分页查询(只查询主表,不查扩展表)
+ start_time = time.time()
+
+ # 计算分页
+ offset = (page - 1) * page_size
+
+ # 查询符合条件的订单总数
+ total_count = Dingdan.objects.filter(query_conditions).count()
+
+ # 查询当前页的数据(只查询主表,只查询前端需要的字段)
+ dingdan_list = Dingdan.objects.filter(query_conditions).only(
+ 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id',
+ 'fadan_pingtai', 'jine', 'tupian', 'create_time'
+ ).order_by('-create_time')[offset:offset + page_size]
+
+ query_time = time.time() - start_time
+ logger.info(f"订单查询完成,耗时: {query_time:.3f}s,查询条数: {len(dingdan_list)}")
+
+ # 8. 构建响应数据(严格按前端需要的字段)
+ result_list = []
+
+ for dingdan in dingdan_list:
+ # 注意:这里字段名必须与前端完全一致
+ order_data = {
+ 'dingdan_id': dingdan.dingdan_id or '',
+ 'jieshao': dingdan.jieshao or '',
+ 'zhuangtai': dingdan.zhuangtai or 1,
+ 'leixing': dingdan.leixing_id or 0, # 前端需要 leixing
+ 'fadanpingtai': dingdan.fadan_pingtai or 1, # 前端需要 fadanpingtai
+ 'jiage': float(dingdan.jine) if dingdan.jine else 0.00, # 价格对应 jine 字段
+ 'tupian_url': dingdan.tupian or '', # 图片对应 tupian 字段
+ 'fadanshijian': dingdan.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan.create_time else ''
+ }
+ result_list.append(order_data)
+
+ # 9. 判断是否有更多数据
+ current_loaded_count = offset + len(result_list)
+ has_more = len(result_list) >= page_size and current_loaded_count < total_count
+
+ # 10. 构建完整响应(严格按前端需要的结构)
+ response_data = {
+ 'stats': {
+ 'platform': platform_stats,
+ 'merchant': merchant_stats
+ },
+ 'list': result_list,
+ 'hasMore': has_more,
+ 'total': total_count,
+ 'currentPage': page,
+ 'pageSize': page_size
+ }
+
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ logger.error(f"获取订单列表异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+
+
+
+
+class AdYaoQingDaShou(APIView):
+ """
+ 后台获取邀请码接口(用于邀请打手)
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /yonghu/adyqds
+ 参数: {
+ "zhanghao": "管理员账号"
+ }
+ 返回: {
+ "code": 0,
+ "message": "成功",
+ "data": {
+ "yaoqingma": "邀请码"
+ }
+ }
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+
+ if not qianzhanghao:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.user_type != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.admin_profile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 获取用户ID(yonghuid),如果获取不到就用'123456'
+ yonghuid_str = getattr(dangqianyonghu, 'yonghuid', '123456')
+
+ # 7. 尝试获取或创建管事扩展表
+ try:
+ # 尝试获取现有的管事扩展表
+ guanshi_profile = dangqianyonghu.guanshi_profile
+ except AttributeError:
+ # 管事扩展表不存在,创建一个
+ try:
+ # 创建默认的管事扩展表
+ guanshi_profile = UserGuanshi.objects.create(
+ user=dangqianyonghu,
+ yaoqingma='', # 暂时为空,后面生成
+ dianhua='', # 默认空
+ wechat='', # 默认空
+ yaogingshuliang=0, # 邀请数量为0
+ zhuangtai=1, # 正常状态
+ jinrichongzhi=0, # 今日充值0
+ jinyuechongzhi=0, # 今月充值0
+ chongzhifenrun=0, # 充值分佣0
+ yue=0 # 余额0
+ )
+ #logger.info(f"为管理员{dangqianyonghu.yonghuid}创建了管事扩展表")
+ except Exception as e:
+ logger.error(f"创建管事扩展表失败: {str(e)}")
+ return Response(
+ {'code': 500, 'message': '创建用户信息失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ # 8. 检查是否已有邀请码
+ existing_yaoqingma = guanshi_profile.yaoqingma
+
+ if existing_yaoqingma and existing_yaoqingma.strip():
+ # 已有邀请码,直接返回
+ return Response({
+ 'code': 0,
+ 'message': '成功获取邀请码',
+ 'data': {
+ 'yaoqingma': existing_yaoqingma
+ }
+ })
+
+ # 9. 生成新的邀请码
+ try:
+ # 生成邀请码
+ new_yaoqingma = CreateInvitationCode(str(yonghuid_str))
+
+ # 验证邀请码格式
+ if not VerifyInvitationCode(new_yaoqingma):
+ raise Exception('邀请码格式验证失败')
+
+ except ImportError:
+ # 如果导入失败,使用简单方法生成邀请码
+ timestamp = str(int(time.time()))
+ random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
+ new_yaoqingma = f"GL{timestamp[-6:]}{random_str}"
+
+ logger.warning(f"使用简单方法生成邀请码: {new_yaoqingma}")
+ except Exception as e:
+ logger.error(f"生成邀请码失败: {str(e)}")
+ return Response(
+ {'code': 500, 'message': '生成邀请码失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ # 10. 保存邀请码到数据库
+ try:
+ with transaction.atomic():
+ guanshi_profile.yaoqingma = new_yaoqingma
+ guanshi_profile.save()
+
+ logger.info(f"管理员{dangqianyonghu.yonghuid}生成邀请码: {new_yaoqingma}")
+ except Exception as e:
+ logger.error(f"保存邀请码失败: {str(e)}")
+ return Response(
+ {'code': 500, 'message': '保存邀请码失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ # 11. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'message': '成功生成邀请码',
+ 'data': {
+ 'yaoqingma': new_yaoqingma
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"获取邀请码接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+
+
+class AdGuanLiYongHu(APIView):
+ """
+ 后台用户管理列表接口
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /yonghu/adgl
+ 参数: {
+ "zhanghao": "管理员账号",
+ "user_type": "用户类型(1-4)",
+ "keyword": "搜索关键词(可选)",
+ "page": "页码(从1开始)",
+ "page_size": "每页数量"
+ }
+ 返回: {
+ "code": 0,
+ "message": "成功",
+ "data": {
+ "list": [用户列表],
+ "stats": {"1": 总数, "2": 总数, "3": 总数, "4": 总数},
+ "has_more": true/false
+ }
+ }
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ user_type = request.data.get('user_type')
+ keyword = request.data.get('keyword', '').strip()
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 30))
+
+ # 验证参数
+ if not qianzhanghao or not user_type:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.user_type != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.admin_profile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 根据用户类型选择查询的模型
+ user_type_int = int(user_type)
+
+ if user_type_int not in [1, 2, 3, 4]:
+ return Response(
+ {'code': 400, 'message': '用户类型错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 7. 统计四种用户类型的总数
+ stats = self._get_user_stats()
+
+ # 8. 根据用户类型查询用户列表
+ user_list, has_more = self._query_users_by_type(
+ user_type_int, keyword, page, page_size
+ )
+
+ # 9. 构建返回数据
+ return Response({
+ 'code': 0,
+ 'message': '成功',
+ 'data': {
+ 'list': user_list,
+ 'stats': stats,
+ 'has_more': has_more
+ }
+ })
+
+ except ValueError:
+ logger.error("参数类型错误")
+ return Response(
+ {'code': 400, 'message': '参数类型错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ except Exception as e:
+ logger.error(f"用户管理列表接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ def _get_user_stats(self):
+ """
+ 获取四种用户类型的统计数量
+ """
+ try:
+
+ stats = {
+ '1': UserBoss.objects.count(),
+ '2': UserDashou.objects.count(),
+ '3': UserGuanshi.objects.count(),
+ '4': UserShangjia.objects.count()
+ }
+
+ return stats
+
+ except Exception as e:
+ logger.error(f"获取用户统计失败: {str(e)}")
+ return {'1': 0, '2': 0, '3': 0, '4': 0}
+
+ def _query_users_by_type(self, user_type_int, keyword, page, page_size):
+ """
+ 根据用户类型查询用户列表
+ 返回: (用户列表, 是否还有更多数据)
+ """
+ try:
+ # 计算偏移量
+ offset = (page - 1) * page_size
+
+ if user_type_int == 1:
+ return self._query_boss_users(keyword, offset, page_size)
+ elif user_type_int == 2:
+ return self._query_dashou_users(keyword, offset, page_size)
+ elif user_type_int == 3:
+ return self._query_guanshi_users(keyword, offset, page_size)
+ elif user_type_int == 4:
+ return self._query_shangjia_users(keyword, offset, page_size)
+ else:
+ return [], False
+
+ except Exception as e:
+ logger.error(f"查询用户列表失败: {str(e)}")
+ return [], False
+
+ def _query_boss_users(self, keyword, offset, limit):
+ """查询普通用户(老板)"""
+ try:
+ queryset = UserBoss.objects.select_related('user')
+
+ if keyword:
+ queryset = queryset.filter(
+ Q(user__yonghuid__icontains=keyword) |
+ Q(nickname__icontains=keyword)
+ )
+
+ # 获取总数
+ total_count = queryset.count()
+
+ # 获取当前页的数据
+ users = list(queryset.order_by('-create_time')[offset:offset + limit])
+
+ # 计算是否有更多数据
+ current_count = len(users)
+ has_more = (offset + current_count) < total_count
+
+
+
+ # 构建返回数据
+ user_list = []
+ for boss in users:
+ user_main = boss.user
+
+ user_data = {
+ 'yonghuid': user_main.yonghuid,
+ 'avatar': user_main.avatar or '',
+ 'nicheng': boss.nickname or '',
+ }
+
+ user_list.append(user_data)
+
+ return user_list, has_more
+
+ except Exception as e:
+ logger.error(f"查询老板用户失败: {str(e)}")
+ return [], False
+
+ def _query_dashou_users(self, keyword, offset, limit):
+ """查询打手用户"""
+ try:
+ queryset = UserDashou.objects.select_related('user')
+
+ if keyword:
+ queryset = queryset.filter(
+ Q(user__yonghuid__icontains=keyword) |
+ Q(nicheng__icontains=keyword) |
+ Q(chenghao__icontains=keyword)
+ )
+
+ # 获取总数
+ total_count = queryset.count()
+
+ # 获取当前页的数据
+ dashou_list = list(queryset.order_by('-create_time')[offset:offset + limit])
+
+ # 计算是否有更多数据
+ current_count = len(dashou_list)
+ has_more = (offset + current_count) < total_count
+
+
+
+ # 构建返回数据
+ user_list = []
+ for dashou in dashou_list:
+ user_main = dashou.user
+
+ user_data = {
+ 'yonghuid': user_main.yonghuid,
+ 'avatar': user_main.avatar or '',
+ 'zaixianzhuangtai': dashou.zaixianzhuangtai,
+ 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
+ 'nicheng': dashou.nicheng or '',
+ 'zhuangtai': dashou.zhuangtai,
+ }
+
+ user_list.append(user_data)
+
+ return user_list, has_more
+
+ except Exception as e:
+ logger.error(f"查询打手用户失败: {str(e)}")
+ return [], False
+
+ def _query_guanshi_users(self, keyword, offset, limit):
+ """查询管事用户"""
+ try:
+ queryset = UserGuanshi.objects.select_related('user')
+
+ if keyword:
+ queryset = queryset.filter(
+ Q(user__yonghuid__icontains=keyword)
+ )
+
+ # 获取总数
+ total_count = queryset.count()
+
+ # 获取当前页的数据
+ guanshi_list = list(queryset.order_by('-create_time')[offset:offset + limit])
+
+ # 计算是否有更多数据
+ current_count = len(guanshi_list)
+ has_more = (offset + current_count) < total_count
+
+
+
+ # 构建返回数据
+ user_list = []
+ for guanshi in guanshi_list:
+ user_main = guanshi.user
+
+ user_data = {
+ 'yonghuid': user_main.yonghuid,
+ 'avatar': user_main.avatar or '',
+ 'zhanghaozhuangtai': guanshi.zhuangtai,
+ 'nicheng': f'管事{user_main.yonghuid}',
+ }
+
+ user_list.append(user_data)
+
+ return user_list, has_more
+
+ except Exception as e:
+ logger.error(f"查询管事用户失败: {str(e)}")
+ return [], False
+
+ def _query_shangjia_users(self, keyword, offset, limit):
+ """查询商家用户"""
+ try:
+ queryset = UserShangjia.objects.select_related('user')
+
+ if keyword:
+ queryset = queryset.filter(
+ Q(user__yonghuid__icontains=keyword) |
+ Q(nicheng__icontains=keyword)
+ )
+
+ # 获取总数
+ total_count = queryset.count()
+
+ # 获取当前页的数据
+ shangjia_list = list(queryset.order_by('-create_time')[offset:offset + limit])
+
+ # 计算是否有更多数据
+ current_count = len(shangjia_list)
+ has_more = (offset + current_count) < total_count
+
+
+
+ # 构建返回数据
+ user_list = []
+ for shangjia in shangjia_list:
+ user_main = shangjia.user
+
+ user_data = {
+ 'yonghuid': user_main.yonghuid,
+ 'avatar': user_main.avatar or '',
+ 'zhanghaozhuangtai': shangjia.zhuangtai,
+ 'nicheng': shangjia.nicheng or '',
+ }
+
+ user_list.append(user_data)
+
+ return user_list, has_more
+
+ except Exception as e:
+ logger.error(f"查询商家用户失败: {str(e)}")
+ return [], False
+
+
+
+
+
+class CfGuanLi(APIView):
+ """
+ 后台处罚管理列表接口
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /yonghu/cfgl
+ 参数: {
+ "zhanghao": "管理员账号",
+ "qingqiu_tongji": true/false, # 是否只请求统计信息
+ "sqzhuangtai": 0 或 [1,2], # 处罚申请状态筛选(可选)
+ "page": "页码(从1开始)", # 分页参数
+ "page_size": "每页数量" # 分页参数
+ }
+ 返回: {
+ "code": 0,
+ "message": "成功",
+ "data": {
+ "list": [处罚记录列表],
+ "zongshu": 总数,
+ "daichuli": 待处理数量,
+ "yichuli": 已处理数量,
+ "haiyougengduo": true/false
+ }
+ }
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ qingqiu_tongji = request.data.get('qingqiu_tongji', False)
+ sqzhuangtai = request.data.get('sqzhuangtai')
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 30))
+
+ # 验证参数
+ if not qianzhanghao:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.user_type != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.admin_profile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 7. 获取统计信息(总是需要统计)
+ zongshu = Chufajilu.objects.count()
+ daichuli = Chufajilu.objects.filter(sqzhuangtai__in=[0, 3]).count()
+ yichuli = Chufajilu.objects.filter(sqzhuangtai__in=[1, 2]).count()
+
+ # 8. 如果只请求统计信息,直接返回
+ if qingqiu_tongji:
+ return Response({
+ 'code': 0,
+ 'message': '成功',
+ 'data': {
+ 'zongshu': zongshu,
+ 'daichuli': daichuli,
+ 'yichuli': yichuli
+ }
+ })
+
+ # 9. 构建查询集
+ queryset = Chufajilu.objects.all()
+
+ # 10. 根据状态筛选
+ if sqzhuangtai is not None:
+ if isinstance(sqzhuangtai, list):
+ # 如果是数组,表示多个状态
+ queryset = queryset.filter(sqzhuangtai__in=sqzhuangtai)
+ else:
+ # 单个状态
+ queryset = queryset.filter(sqzhuangtai=sqzhuangtai)
+
+ # 11. 获取总数(用于分页计算)
+ total_count = queryset.count()
+
+ # 12. 计算分页偏移量
+ offset = (page - 1) * page_size
+
+ # 13. 分页查询
+ chufa_list = list(queryset.order_by('-create_time')[offset:offset + page_size])
+
+ # 🔴【新增】高效获取图片信息
+ if chufa_list:
+ # 收集所有需要查询的组合
+ dingdan_ids = []
+ yonghu_combinations = [] # 保存 (dingdan_id, yonghuid) 组合
+
+ for chufa in chufa_list:
+ if chufa.dingdan_id:
+ dingdan_ids.append(chufa.dingdan_id)
+ # 商家图片组合
+ if chufa.qingqiuid:
+ yonghu_combinations.append((chufa.dingdan_id, chufa.qingqiuid))
+ # 打手图片组合
+ if chufa.dashouid:
+ yonghu_combinations.append((chufa.dingdan_id, chufa.dashouid))
+
+ # 去重
+ dingdan_ids = list(set(dingdan_ids))
+ yonghu_combinations = list(set(yonghu_combinations))
+
+ # 🔴【新增】批量查询图片表(高效查询,避免循环)
+ tupian_mapping = {}
+ if yonghu_combinations:
+ # 构建查询条件
+ query_conditions = Q()
+ for dingdan_id, yonghuid in yonghu_combinations:
+ query_conditions |= Q(dingdan_id=dingdan_id, yonghuid=yonghuid)
+
+ # 批量查询
+ tupian_queryset = Chufatupian.objects.filter(query_conditions).values(
+ 'dingdan_id', 'yonghuid', 'tupian'
+ )
+
+ # 构建映射:key为"dingdan_id_yonghuid",value为图片URL列表
+ for item in tupian_queryset:
+ key = f"{item['dingdan_id']}_{item['yonghuid']}"
+ if key not in tupian_mapping:
+ tupian_mapping[key] = []
+ if item['tupian']:
+ tupian_mapping[key].append(item['tupian'])
+ else:
+ tupian_mapping = {}
+
+ # 🔴【修改】14. 构建返回数据 - 添加图片和申诉理由字段
+ chufa_data = []
+ for chufa in chufa_list:
+ # 🔴【新增】获取商家证据图片
+ zhengju_tupian = []
+ if chufa.dingdan_id and chufa.qingqiuid:
+ key = f"{chufa.dingdan_id}_{chufa.qingqiuid}"
+ zhengju_tupian = tupian_mapping.get(key, [])
+
+ # 🔴【新增】获取打手申诉图片
+ shensu_tupian = []
+ if chufa.dingdan_id and chufa.dashouid:
+ key = f"{chufa.dingdan_id}_{chufa.dashouid}"
+ shensu_tupian = tupian_mapping.get(key, [])
+
+ chufa_data.append({
+ 'dashouid': chufa.dashouid,
+ 'qingqiuid': chufa.qingqiuid,
+ 'chuliid': chufa.chuliid,
+ 'cfliyou': chufa.cfliyou,
+ 'sqzhuangtai': chufa.sqzhuangtai,
+ 'bhliyou': chufa.bhliyou,
+ 'jifen': chufa.jifen,
+ 'dingdan_id': chufa.dingdan_id,
+ 'create_time': chufa.create_time,
+ 'update_time': chufa.update_time,
+ # 🔴【新增】申诉理由字段
+ 'ssliyou': chufa.ssliyou or '',
+ # 🔴【新增】商家证据图片
+ 'zhengju_tupian': zhengju_tupian,
+ # 🔴【新增】打手申诉图片
+ 'shensu_tupian': shensu_tupian,
+ })
+
+ # 15. 计算是否还有更多数据
+ current_count = len(chufa_list)
+ haiyougengduo = (offset + current_count) < total_count
+
+ return Response({
+ 'code': 0,
+ 'message': '成功',
+ 'data': {
+ 'list': chufa_data,
+ 'zongshu': zongshu,
+ 'daichuli': daichuli,
+ 'yichuli': yichuli,
+ 'haiyougengduo': haiyougengduo
+ }
+ })
+
+ except ValueError:
+ return Response(
+ {'code': 400, 'message': '参数类型错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ except Exception as e:
+ logger.error(f"处罚管理列表接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+logger = logging.getLogger(__name__)
+
+
+class AdTongYiChuFa(APIView):
+ """
+ 后台同意/驳回处罚接口(已修复重复记录问题)
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /yonghu/adtycf
+ 参数: {
+ "zhanghao": "管理员账号",
+ "dingdan_id": "订单ID",
+ "caozuo": 1或2, # 1=同意处罚,2=拒绝处罚
+ "bohui_liyou": "拒绝理由" # 拒绝处罚时必填
+ }
+ 返回: {
+ "code": 0,
+ "message": "成功",
+ "data": null
+ }
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ caozuo = request.data.get('caozuo') # 1=同意,2=拒绝
+ bohui_liyou = request.data.get('bohui_liyou', '').strip()
+
+ # 验证参数
+ if not qianzhanghao or not dingdan_id or caozuo is None:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证操作类型
+ if caozuo not in [1, 2]:
+ return Response(
+ {'code': 400, 'message': '操作类型错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 如果是拒绝处罚,需要拒绝理由
+ if caozuo == 2 and not bohui_liyou:
+ return Response(
+ {'code': 400, 'message': '拒绝处罚需要填写理由', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.user_type != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.admin_profile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 使用事务保证数据一致性
+ with transaction.atomic():
+ # 8. 🔴 修复:查询处罚记录(处理重复记录问题)
+ # 使用 filter().first() 而不是 get(),以防出现多条记录
+ chufajilu = Chufajilu.objects.filter(
+ dingdan_id=dingdan_id,
+ sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中状态
+ ).order_by('-create_time').first() # 取最新的一条
+
+ if not chufajilu:
+ # 如果没有待处理的处罚记录,检查是否有已处理的
+ exists_processed = Chufajilu.objects.filter(dingdan_id=dingdan_id).exists()
+ if exists_processed:
+ return Response(
+ {'code': 400, 'message': '该处罚记录已处理', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ else:
+ return Response(
+ {'code': 404, 'message': '处罚记录不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 9. 验证处罚记录状态(必须是待处理或申诉中状态才能处理)
+ if chufajilu.sqzhuangtai not in [0, 3]: # 🔴【修改】包括待处理(0)和申诉中(3)
+ return Response(
+ {'code': 400, 'message': '该处罚记录已处理', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 10. 🔴 修复:检查是否有重复的待处理/申诉中记录,并记录警告
+ duplicate_count = Chufajilu.objects.filter(
+ dingdan_id=dingdan_id,
+ sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中
+ ).count()
+
+ if duplicate_count > 1:
+ # 记录警告日志
+ logger.warning(f"订单ID {dingdan_id} 存在 {duplicate_count} 条待处理/申诉中处罚记录,处理最新的一条")
+
+ # 🔴 可选:将其他重复的待处理/申诉中记录标记为已拒绝
+ # 防止用户重复提交相同订单的处罚申请
+ duplicate_records = Chufajilu.objects.filter(
+ dingdan_id=dingdan_id,
+ sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中
+ ).exclude(id=chufajilu.id)
+
+ for dup in duplicate_records:
+ dup.sqzhuangtai = 2 # 已拒绝
+ dup.chuliid = qianzhanghao
+ dup.bhliyou = "系统自动拒绝:存在重复处罚记录"
+ dup.save()
+
+ # 11. 根据操作类型处理
+ if caozuo == 1:
+ # 同意处罚 - 不扣分,只改状态
+ # 11.1 检查打手是否存在
+ if not chufajilu.dashouid:
+ return Response(
+ {'code': 400, 'message': '被处罚打手ID不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 11.2 查询打手扩展表(只是验证存在性)
+ try:
+ dashou = UserDashou.objects.get(user__yonghuid=chufajilu.dashouid)
+ except UserDashou.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '被处罚打手不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 🔴【修改】11.3 同意处罚时不扣分(因为申请时已经扣了),只更新状态
+ chufajilu.sqzhuangtai = 1 # 已处罚
+ chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号
+ chufajilu.save()
+
+ # 11.4 更新商家订单扩展表
+ try:
+ dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
+ if hasattr(dingdan, 'shangjia_kuozhan'):
+ shangjia_kuozhan = dingdan.shangjia_kuozhan
+ shangjia_kuozhan.sqzhuangtai = 1 # 已处罚
+ shangjia_kuozhan.save()
+ except (Dingdan.DoesNotExist, AttributeError):
+ # 如果订单或扩展表不存在,继续执行,不中断
+ pass
+
+ logger.info(f"同意处罚成功:订单ID={dingdan_id},打手ID={chufajilu.dashouid}")
+
+ elif caozuo == 2:
+ # 拒绝处罚 - 需要返还积分(加分)
+ # 🔴【新增】11.5 返还打手积分(加5分)
+ if chufajilu.jifen > 0:
+ try:
+ dashou = UserDashou.objects.get(user__yonghuid=chufajilu.dashouid)
+
+ # 🔴【新增】安全验证:必须积分小于等于5分才允许加分
+ if dashou.jifen <= 5: # 判断积分是否小于等于5分
+ dashou.jifen += chufajilu.jifen # 加5分
+ dashou.save()
+ logger.info(
+ f"拒绝处罚,给打手{dashou.user.yonghuid}返还积分{chufajilu.jifen}分,当前积分{dashou.jifen}")
+ else:
+ # 积分已超过5分,可能是异常情况,记录警告但不中断处理
+ logger.warning(
+ f"拒绝处罚时打手积分异常:打手ID={chufajilu.dashouid},当前积分{dashou.jifen}已超过5分,不再加分")
+ except UserDashou.DoesNotExist:
+ # 打手不存在,记录错误,但继续处理状态
+ logger.error(f"拒绝处罚时打手不存在:打手ID={chufajilu.dashouid}")
+
+ # 11.6 更新处罚记录
+ chufajilu.sqzhuangtai = 2 # 已拒绝
+ chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号
+ chufajilu.bhliyou = bohui_liyou # 拒绝理由
+ chufajilu.save()
+
+ # 11.7 更新商家订单扩展表
+ try:
+ dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
+ if hasattr(dingdan, 'shangjia_kuozhan'):
+ shangjia_kuozhan = dingdan.shangjia_kuozhan
+ shangjia_kuozhan.sqzhuangtai = 2 # 已拒绝
+ shangjia_kuozhan.bhliyou = bohui_liyou # 拒绝理由
+ shangjia_kuozhan.save()
+ except (Dingdan.DoesNotExist, AttributeError):
+ # 如果订单或扩展表不存在,继续执行,不中断
+ pass
+
+ logger.info(f"拒绝处罚成功:订单ID={dingdan_id},拒绝理由={bohui_liyou}")
+
+ # 12. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'message': '处理成功',
+ 'data': None
+ })
+
+ except ValueError:
+ return Response(
+ {'code': 400, 'message': '参数类型错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ except Exception as e:
+ logger.error(f"处理处罚接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+
+
+# yonghu/views.py
+class AdckyhxqView(APIView):
+ """
+ 获取用户详情接口
+ 前端需要传递: zhanghao, uid, user_type
+ 返回用户信息和会员列表
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # ==================== 管理员身份验证 ====================
+ # 1. 验证JWT Token
+ if not request.user.is_authenticated:
+ return Response({
+ 'code': 401,
+ 'message': '未认证',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 验证用户类型为管理员
+ if request.user.user_type != 'admin':
+ return Response({
+ 'code': 403,
+ 'message': '权限不足',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 3. 验证管理员扩展表存在
+ admin_profile = getattr(request.user, 'admin_profile', None)
+ if not admin_profile:
+ return Response({
+ 'code': 403,
+ 'message': '管理员信息不完整',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 4. 验证管理员账号(phone)与前端传递的zhanghao一致
+ zhanghao = request.data.get('zhanghao')
+ if not zhanghao or request.user.phone != zhanghao:
+ return Response({
+ 'code': 401,
+ 'message': '账号验证失败',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+ # ==================== 身份验证结束 ====================
+
+ # 获取业务参数
+ uid = request.data.get('uid') # 用户ID (yonghuid)
+ user_type = request.data.get('user_type') # 用户类型 (1,2,3,4)
+
+ if not uid or not user_type:
+ return Response({
+ 'code': 400,
+ 'message': '参数缺失',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证用户类型
+ try:
+ user_type = int(user_type)
+ if user_type not in [1, 2, 3, 4]:
+ raise ValueError
+ except ValueError:
+ return Response({
+ 'code': 400,
+ 'message': '用户类型无效',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 查询用户主表 - 使用select_related优化查询
+ user_main = UserMain.objects.filter(yonghuid=uid).first()
+ if not user_main:
+ return Response({
+ 'code': 404,
+ 'message': '用户不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 构建响应数据
+ response_data = {
+ 'user_info': {},
+ 'member_list': [],
+ 'all_member_list': []
+ }
+
+ # 通用字段(所有用户都有)
+ user_info = {
+ 'yonghuid': user_main.yonghuid,
+ 'avatar': user_main.avatar,
+ 'phone': user_main.phone,
+ 'ip': user_main.ip,
+ 'create_time': user_main.create_time,
+ 'update_time': user_main.update_time,
+ 'user_type': user_type
+ }
+
+ # 根据用户类型查询扩展信息
+ if user_type == 1: # 老板
+ # 使用select_related预取boss_profile
+ user_main = UserMain.objects.filter(yonghuid=uid).select_related('boss_profile').first()
+ if user_main.boss_profile:
+ boss_profile = user_main.boss_profile
+ user_info.update({
+ 'nickname': boss_profile.nickname,
+ 'zonge': boss_profile.zonge,
+ 'alldingdan': boss_profile.alldingdan,
+ 'alltui': boss_profile.alltui,
+ 'create_time_boss': boss_profile.create_time
+ })
+
+ elif user_type == 2: # 打手
+ # 使用select_related预取dashou_profile
+ user_main = UserMain.objects.filter(yonghuid=uid).select_related('dashou_profile').first()
+ if user_main.dashou_profile:
+ dashou_profile = user_main.dashou_profile
+
+ # 构建打手信息
+ user_info.update({
+ 'nicheng': dashou_profile.nicheng,
+ 'chenghao': dashou_profile.chenghao,
+ 'zhuangtai': dashou_profile.zhuangtai,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
+ 'jieshao': dashou_profile.jieshao,
+ 'jiedanzongliang': dashou_profile.jiedanzongliang,
+ 'chengjiaozongliang': dashou_profile.chengjiaozongliang,
+ 'tuikuanliang': dashou_profile.tuikuanliang,
+ 'yue': dashou_profile.yue, # 打手可提现余额
+ 'zonge': dashou_profile.zonge, # 打手赚取总额
+ 'dianhua': dashou_profile.dianhua,
+ 'wechat': dashou_profile.wechat,
+ 'yaoqingren': dashou_profile.yaoqingren,
+ 'jinrijiedan': dashou_profile.jinrijiedan,
+ 'jinrishouyi': dashou_profile.jinrishouyi,
+ 'jinyuejiedan': dashou_profile.jinyuejiedan,
+ 'jinyueshouyi': dashou_profile.jinyueshouyi,
+ 'jifen': dashou_profile.jifen,
+ 'yajin': dashou_profile.yajin,
+ 'create_time_dashou': dashou_profile.create_time
+ })
+
+ # 查询打手的会员信息(只返回未过期的)
+ # 使用select_related预取huiyuan信息(如果有关联的话)
+ member_records = Huiyuangoumai.objects.filter(
+ yonghu_id=uid
+ ).order_by('-create_time')
+
+ member_list = []
+ for record in member_records:
+ # 调用方法检查是否过期,并更新状态
+ is_expired = record.jiance_shifou_daoqi()
+
+ if not is_expired: # 只返回未过期的
+ # 查询会员详情
+ huiyuan = Huiyuan.objects.filter(
+ huiyuan_id=record.huiyuan_id
+ ).first()
+
+ if huiyuan:
+ member_list.append({
+ 'huiyuan_id': record.huiyuan_id,
+ 'huiyuan_name': huiyuan.jieshao,
+ 'daoqi_time': record.daoqi_time,
+ 'is_active': record.huiyuan_zhuangtai == 1,
+ 'create_time': record.create_time
+ })
+
+ response_data['member_list'] = member_list
+
+ elif user_type == 3: # 管事
+ # 使用select_related预取guanshi_profile
+ user_main = UserMain.objects.filter(yonghuid=uid).select_related('guanshi_profile').first()
+ if user_main.guanshi_profile:
+ guanshi_profile = user_main.guanshi_profile
+ user_info.update({
+ 'nicheng': user_main.phone, # 管事昵称用手机号
+ 'yaoqingma': guanshi_profile.yaoqingma,
+ 'dianhua': guanshi_profile.dianhua,
+ 'wechat': guanshi_profile.wechat,
+ 'yaogingshuliang': guanshi_profile.yaogingshuliang,
+ 'zhuangtai': guanshi_profile.zhuangtai,
+ 'jinrichongzhi': guanshi_profile.jinrichongzhi,
+ 'jinyuechongzhi': guanshi_profile.jinyuechongzhi,
+ 'chongzhifenrun': guanshi_profile.chongzhifenrun,
+ 'yue': guanshi_profile.yue, # 管事可提现余额
+ 'create_time_guanshi': guanshi_profile.create_time
+ })
+
+ elif user_type == 4: # 商家
+ # 使用select_related预取shop_profile
+ user_main = UserMain.objects.filter(yonghuid=uid).select_related('shop_profile').first()
+ if user_main.shop_profile:
+ shop_profile = user_main.shop_profile
+ user_info.update({
+ 'nicheng': shop_profile.nicheng,
+ 'zhuangtai': shop_profile.zhuangtai,
+ 'dianhua': shop_profile.dianhua,
+ 'wechat': shop_profile.wechat,
+ 'fabu': shop_profile.fabu,
+ 'tuikuan': shop_profile.tuikuan,
+ 'yue': shop_profile.yue, # 商家余额
+ 'chengjiao': shop_profile.chengjiao,
+ 'jinridingdan': shop_profile.jinridingdan,
+ 'jinriliushui': shop_profile.jinriliushui,
+ 'jinyuedingdan': shop_profile.jinyuedingdan,
+ 'jinyueliushui': shop_profile.jinyueliushui,
+ 'create_time_shop': shop_profile.create_time
+ })
+
+ # 获取所有会员列表(用于前端下拉选择)
+ all_huiyuan = Huiyuan.objects.all().order_by('jiage')
+ all_member_list = [
+ {
+ 'id': huiyuan.huiyuan_id,
+ 'nicheng': huiyuan.jieshao,
+ 'jiage': huiyuan.jiage,
+ 'jieshao': huiyuan.jtjieshao
+ }
+ for huiyuan in all_huiyuan
+ ]
+
+ response_data['user_info'] = user_info
+ response_data['all_member_list'] = all_member_list
+
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': response_data
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ return Response({
+ 'code': 500,
+ 'message': f'获取用户详情失败: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class AdcjxgView(APIView):
+ """
+ 修改用户信息接口
+ 前端需要传递: zhanghao, uid, user_type, 以及其他修改的字段
+ 支持打手会员添加功能
+ 敏感字段修改会记录到修改记录表
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # ==================== 管理员身份验证 ====================
+ if not request.user.is_authenticated:
+ return Response({
+ 'code': 401,
+ 'message': '未认证',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ if request.user.user_type != 'admin':
+ return Response({
+ 'code': 403,
+ 'message': '权限不足',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ admin_profile = getattr(request.user, 'admin_profile', None)
+ if not admin_profile:
+ return Response({
+ 'code': 403,
+ 'message': '管理员信息不完整',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ zhanghao = request.data.get('zhanghao')
+ if not zhanghao or request.user.phone != zhanghao:
+ return Response({
+ 'code': 401,
+ 'message': '账号验证失败',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+ # ==================== 身份验证结束 ====================
+
+ # 获取业务参数
+ uid = request.data.get('uid')
+ user_type = request.data.get('user_type')
+
+ if not uid or not user_type:
+ return Response({
+ 'code': 400,
+ 'message': '参数缺失',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ user_type = int(user_type)
+ if user_type not in [1, 2, 3, 4]:
+ raise ValueError
+ except ValueError:
+ return Response({
+ 'code': 400,
+ 'message': '用户类型无效',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 查询用户主表
+ user_main = UserMain.objects.filter(yonghuid=uid).first()
+ if not user_main:
+ return Response({
+ 'code': 404,
+ 'message': '用户不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 使用事务确保数据一致性
+ with transaction.atomic():
+ # 存储修改记录的数据
+ modify_record_data = {
+ 'yonghuid': uid,
+ 'xiugaiid': zhanghao,
+ 'leixing': user_type,
+ 'qitashuoming': '',
+ 'create_time': timezone.now()
+ }
+
+ # 标记是否有敏感字段被修改
+ has_sensitive_change = False
+
+ if user_type == 1: # 老板 - 不允许修改
+ return Response({
+ 'code': 403,
+ 'message': '老板信息不允许修改',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ elif user_type == 2: # 打手
+ # 获取或创建打手扩展记录
+ dashou_profile, created = UserDashou.objects.get_or_create(
+ user=user_main,
+ defaults={'nicheng': user_main.phone or f'打手{uid}'}
+ )
+
+ # 记录敏感字段的旧值
+ old_yue = dashou_profile.yue # 打手可提现余额
+ old_zonge = dashou_profile.zonge # 打手提现金额
+ old_jifen = dashou_profile.jifen # 打手积分
+ old_yajin = dashou_profile.yajin # 打手押金
+
+ # 需要更新的字段列表
+ update_fields = [
+ 'chenghao', 'dianhua', 'yue', 'zonge', 'jifen',
+ 'yajin', 'yaoqingren', 'jieshao', 'zhuangtai',
+ 'zaixianzhuangtai', 'zhanghaozhuangtai'
+ ]
+
+ # 记录被修改的字段
+ modified_fields = []
+
+ for field in update_fields:
+ if field in request.data:
+ new_value = request.data.get(field)
+ old_value = getattr(dashou_profile, field)
+
+ # 检查值是否改变
+ if str(new_value) != str(old_value):
+ # 处理特殊字段类型
+ if field in ['yue', 'zonge', 'yajin']:
+ new_value = Decimal(str(new_value))
+ setattr(dashou_profile, field, new_value)
+ elif field in ['jifen']:
+ new_value = int(new_value)
+ setattr(dashou_profile, field, new_value)
+ elif field in ['zhuangtai', 'zaixianzhuangtai', 'zhanghaozhuangtai']:
+ new_value = int(new_value)
+ setattr(dashou_profile, field, new_value)
+ else:
+ setattr(dashou_profile, field, new_value)
+
+ modified_fields.append(field)
+
+ # 记录敏感字段修改
+ if field == 'yue':
+ has_sensitive_change = True
+ modify_record_data['xiugaitijiao'] = new_value # 使用xiugaitijiao字段记录打手余额修改
+ modify_record_data['xiugaitijiaoq'] = old_yue
+ elif field == 'zonge':
+ has_sensitive_change = True
+ # zonge字段在修改记录表中可能没有直接对应,可以记录在备注中
+ modify_record_data['qitashuoming'] += f' 打手总额从{old_zonge}修改为{new_value}'
+ elif field == 'jifen':
+ has_sensitive_change = True
+ modify_record_data['jifen'] = new_value
+ modify_record_data['yjifen'] = old_jifen
+ elif field == 'yajin':
+ has_sensitive_change = True
+ modify_record_data['yajin'] = new_value
+ modify_record_data['yyajin'] = old_yajin
+
+ # 保存打手信息
+ dashou_profile.save()
+
+ # 记录被修改的非敏感字段
+ non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'zonge', 'jifen', 'yajin']]
+ if non_sensitive_fields:
+ modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
+
+ elif user_type == 3: # 管事
+ # 获取或创建管事扩展记录
+ guanshi_profile, created = UserGuanshi.objects.get_or_create(
+ user=user_main,
+ defaults={'yaoqingma': f'GS{uid[-4:]}'}
+ )
+
+ # 记录敏感字段的旧值
+ old_yue = guanshi_profile.yue # 管事可提现余额
+
+ update_fields = [
+ 'yaogingshuliang', 'chongzhifenrun', 'yue',
+ 'zhuangtai', 'dianhua', 'wechat'
+ ]
+
+ modified_fields = []
+
+ for field in update_fields:
+ if field in request.data:
+ new_value = request.data.get(field)
+ old_value = getattr(guanshi_profile, field)
+
+ if str(new_value) != str(old_value):
+ if field in ['chongzhifenrun', 'yue']:
+ new_value = Decimal(str(new_value))
+ setattr(guanshi_profile, field, new_value)
+ elif field in ['yaogingshuliang']:
+ new_value = int(new_value)
+ setattr(guanshi_profile, field, new_value)
+ elif field in ['zhuangtai']:
+ new_value = int(new_value)
+ setattr(guanshi_profile, field, new_value)
+ else:
+ setattr(guanshi_profile, field, new_value)
+
+ modified_fields.append(field)
+
+ # 记录敏感字段修改
+ if field == 'yue':
+ has_sensitive_change = True
+ modify_record_data['guanshiyue'] = new_value
+ modify_record_data['guanshiyueq'] = old_yue
+ elif field == 'chongzhifenrun':
+ has_sensitive_change = True
+ modify_record_data['qitashuoming'] += f' 管事分佣从{old_value}修改为{new_value}'
+
+ guanshi_profile.save()
+
+ # 记录非敏感字段
+ non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'chongzhifenrun']]
+ if non_sensitive_fields:
+ modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
+
+ elif user_type == 4: # 商家
+ # 获取或创建商家扩展记录
+ shop_profile, created = UserShangjia.objects.get_or_create(
+ user=user_main,
+ defaults={'nicheng': user_main.phone or f'商家{uid}'}
+ )
+
+ # 记录敏感字段的旧值
+ old_yue = shop_profile.yue # 商家余额
+
+ update_fields = ['yue', 'zhuangtai', 'dianhua', 'wechat']
+
+ modified_fields = []
+
+ for field in update_fields:
+ if field in request.data:
+ new_value = request.data.get(field)
+ old_value = getattr(shop_profile, field)
+
+ if str(new_value) != str(old_value):
+ if field == 'yue':
+ new_value = Decimal(str(new_value))
+ setattr(shop_profile, field, new_value)
+ elif field == 'zhuangtai':
+ new_value = int(new_value)
+ setattr(shop_profile, field, new_value)
+ else:
+ setattr(shop_profile, field, new_value)
+
+ modified_fields.append(field)
+
+ # 记录敏感字段修改
+ if field == 'yue':
+ has_sensitive_change = True
+ modify_record_data['shangjiayue'] = new_value
+ modify_record_data['shangjiayueq'] = old_yue
+
+ shop_profile.save()
+
+ # 记录非敏感字段
+ non_sensitive_fields = [f for f in modified_fields if f != 'yue']
+ if non_sensitive_fields:
+ modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
+
+ # 处理打手会员添加(只有打手可以添加会员)
+
+ # 🔥🔥🔥 后端修改:支持正负数会员天数
+ # 修改会员处理逻辑部分
+
+ if user_type == 2 and 'huiyuan_id' in request.data and 'days' in request.data:
+ huiyuan_id = request.data.get('huiyuan_id')
+ days = int(request.data.get('days', 0))
+
+ # 🔥 修改:允许正负数,但不能为0
+ if huiyuan_id and days != 0:
+ # 🔥 修改:检查绝对值范围
+ if abs(days) < 1 or abs(days) > 10000:
+ return Response({
+ 'code': 400,
+ 'message': '会员天数必须在1-10000之间(正数增加,负数减少)',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 检查会员是否存在
+ huiyuan = Huiyuan.objects.filter(huiyuan_id=huiyuan_id).first()
+ if not huiyuan:
+ return Response({
+ 'code': 404,
+ 'message': '会员不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 检查是否已购买该会员
+ existing_record = Huiyuangoumai.objects.filter(
+ yonghu_id=uid,
+ huiyuan_id=huiyuan_id
+ ).first()
+
+ if existing_record:
+ # 🔥🔥🔥 重要修改:支持减少天数
+ # 计算新的到期时间
+ new_daoqi_time = existing_record.daoqi_time + timedelta(days=days)
+
+ # 🔥 安全验证:减少天数后不能小于当前时间前90天(防止过度减少)
+ # 这个限制可以根据业务调整
+ min_allowed_time = timezone.now() - timedelta(days=90)
+ if new_daoqi_time < min_allowed_time:
+ return Response({
+ 'code': 400,
+ 'message': f'减少天数过多,调整后到期时间不能早于 {(min_allowed_time + timedelta(days=90)).strftime("%Y-%m-%d")}',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ existing_record.daoqi_time = new_daoqi_time
+ existing_record.save()
+
+ # 记录会员操作
+ has_sensitive_change = True
+ modify_record_data['huiyuants'] = days
+ modify_record_data['huiyuan_id'] = huiyuan_id
+
+ if days > 0:
+ modify_record_data['qitashuoming'] += f' 续费会员 {huiyuan.jieshao} {days}天'
+ # 只有增加天数时才增加购买次数
+ huiyuan.goumai_cishu += 1
+ huiyuan.save()
+ else:
+ modify_record_data['qitashuoming'] += f' 减少会员 {huiyuan.jieshao} {abs(days)}天'
+ # 减少天数时不增加购买次数
+
+ else:
+ # 🔥 修改:新购买时只能增加天数
+ if days > 0:
+ daoqi_time = timezone.now() + timedelta(days=days)
+ new_record = Huiyuangoumai.objects.create(
+ huiyuan_id=huiyuan_id,
+ yonghu_id=uid,
+ jieshao=huiyuan.jieshao,
+ daoqi_time=daoqi_time
+ )
+
+ # 记录会员购买
+ has_sensitive_change = True
+ modify_record_data['huiyuants'] = days
+ modify_record_data['huiyuan_id'] = huiyuan_id
+ modify_record_data['qitashuoming'] += f' 购买会员 {huiyuan.jieshao} {days}天'
+
+ # 更新会员购买次数
+ huiyuan.goumai_cishu += 1
+ huiyuan.save()
+ else:
+ # 🔥 不能为不存在的会员减少天数
+ return Response({
+ 'code': 400,
+ 'message': '用户未购买该会员,无法减少天数',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+
+
+
+
+
+
+
+
+
+
+ # 如果有敏感字段被修改或会员操作,创建修改记录
+ if has_sensitive_change or 'huiyuan_id' in request.data:
+ # 创建修改记录
+ Xiugaijilu.objects.create(**modify_record_data)
+ elif modify_record_data.get('qitashuoming', '').strip():
+ # 只有非敏感字段修改,也记录但qitashuoming不同
+ Xiugaijilu.objects.create(**modify_record_data)
+
+ return Response({
+ 'code': 0,
+ 'message': '修改成功',
+ 'data': None
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ return Response({
+ 'code': 500,
+ 'message': f'修改失败: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+# 管理员认证装饰器
+def admin_required(view_func):
+ """管理员权限验证装饰器"""
+
+ def wrapped_view(self, request, *args, **kwargs):
+ try:
+ # 1. 验证JWT Token
+ if not request.user.is_authenticated:
+ return Response({
+ 'code': 401,
+ 'message': '未认证',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 验证用户类型为管理员
+ if request.user.user_type != 'admin':
+ return Response({
+ 'code': 403,
+ 'message': '权限不足',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 3. 验证管理员扩展表存在
+ admin_profile = getattr(request.user, 'admin_profile', None)
+ if not admin_profile:
+ return Response({
+ 'code': 403,
+ 'message': '管理员信息不完整',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 4. 验证管理员账号(phone)与前端传递的zhanghao一致
+ zhanghao = request.data.get('zhanghao')
+ if not zhanghao or request.user.phone != zhanghao:
+ return Response({
+ 'code': 401,
+ 'message': '账号验证失败',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ return view_func(self, request, *args, **kwargs)
+ except Exception as e:
+ return Response({
+ 'code': 500,
+ 'message': f'权限验证异常: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ return wrapped_view
+
+
+class AddtxshView(APIView):
+ """
+ 获取提现审核列表接口
+ 与用户管理页面保持一致的分页逻辑
+ 支持按用户ID搜索,排序改为按申请时间升序(最早申请排最前)
+ """
+ permission_classes = [IsAuthenticated]
+
+ @admin_required
+ def post(self, request):
+ try:
+ # 获取参数
+ zhanghao = request.data.get('zhanghao')
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 5))
+ zhuangtai = int(request.data.get('zhuangtai', 1))
+ leixing = int(request.data.get('leixing', 1))
+ fangshi = request.data.get('fangshi')
+
+ # 🔴【新增】获取搜索参数(用户ID)
+ search_uid = request.data.get('search_uid', '').strip()
+
+ if page < 1:
+ page = 1
+
+ # 计算偏移量(与用户管理页面完全一致)
+ offset = (page - 1) * page_size
+
+ # 构建查询条件
+ query = Q()
+
+ # 状态筛选
+ if zhuangtai == 1:
+ query &= Q(zhuangtai=1)
+ elif zhuangtai == 2:
+ query &= Q(zhuangtai__in=[2, 3])
+
+ # 类型筛选
+ query &= Q(leixing=leixing)
+
+ # 收款方式筛选(可选)
+ if fangshi:
+ query &= Q(fangshi=int(fangshi))
+
+ # 🔴【新增】如果搜索了用户ID,添加条件
+ if search_uid:
+ query &= Q(yonghuid=search_uid)
+
+ # 🔴【修改】排序改为按申请时间升序(最早申请排最前)
+ queryset = Tixianjilu.objects.filter(query).order_by('create_time')
+
+ # 🔴【修改】分页逻辑:如果是搜索模式,不分页,返回所有匹配记录
+ if search_uid:
+ records = list(queryset)
+ total_count = len(records)
+ has_more = False # 搜索模式没有更多数据
+ else:
+ total_count = queryset.count()
+ records = list(queryset[offset:offset + page_size])
+ current_count = len(records)
+ has_more = (offset + current_count) < total_count
+
+ # 获取状态统计(统计与搜索无关,依然按筛选条件统计)
+ status_counts = {
+ 'awaiting': Tixianjilu.objects.filter(
+ zhuangtai=1,
+ leixing=leixing
+ ).count(),
+ 'processed': Tixianjilu.objects.filter(
+ zhuangtai__in=[2, 3],
+ leixing=leixing
+ ).count()
+ }
+
+ # 构建响应数据
+ withdrawal_list = []
+ for record in records:
+ withdrawal_list.append({
+ 'id': record.id,
+ 'yonghuid': record.yonghuid,
+ 'avatar': record.avatar,
+ 'phone': record.phone,
+ 'nicheng': record.nicheng,
+ 'leixing': record.leixing,
+ 'zhifu': record.zhifu,
+ 'skzhanghao': record.skzhanghao,
+ 'jine': str(record.jine) if record.jine else '0.00',
+ 'zhuangtai': record.zhuangtai,
+ 'fangshi': record.fangshi,
+ 'shenheid': record.shenheid,
+ 'bhliyou': record.bhliyou,
+ 'create_time': record.create_time,
+ 'update_time': record.update_time
+ })
+
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': {
+ 'list': withdrawal_list,
+ 'total_count': total_count,
+ 'status_counts': status_counts,
+ 'has_more': has_more
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ return Response({
+ 'code': 500,
+ 'message': f'获取提现列表失败: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+class AdtixianqkView(APIView):
+ """
+ 处理提现申请接口(已更新:添加收支记录表更新)
+ """
+ permission_classes = [IsAuthenticated]
+
+ @admin_required # 假设你有这个装饰器
+ def post(self, request):
+ try:
+ zhanghao = request.data.get('zhanghao')
+ tixian_id = request.data.get('tixian_id')
+ result = int(request.data.get('result', 0))
+ reason = request.data.get('reason', '')
+
+ if not tixian_id:
+ return Response({
+ 'code': 400,
+ 'message': '提现记录ID不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if result not in [2, 3]:
+ return Response({
+ 'code': 400,
+ 'message': '操作结果无效',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 如果是拒绝,需要理由
+ if result == 3 and not reason:
+ return Response({
+ 'code': 400,
+ 'message': '拒绝提现需要提供理由',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取提现记录
+ try:
+ withdrawal = Tixianjilu.objects.get(id=tixian_id)
+ except Tixianjilu.DoesNotExist:
+ return Response({
+ 'code': 404,
+ 'message': '提现记录不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 检查状态,只有待审核的可以处理
+ if withdrawal.zhuangtai != 1:
+ return Response({
+ 'code': 400,
+ 'message': '该提现申请已被处理',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 使用事务确保数据一致性
+ with transaction.atomic():
+ # 更新提现记录
+ withdrawal.zhuangtai = result
+ withdrawal.shenheid = request.user.yonghuid
+
+ if result == 3: # 拒绝
+ withdrawal.bhliyou = reason
+
+ withdrawal.update_time = timezone.now()
+ withdrawal.save()
+
+ # 🟢 新增:如果同意提现(result=2),更新收支记录表
+ if result == 2:
+ try:
+ # 查询收支记录表,id=1的记录
+ szjilu_record = Szjilu.objects.get(id=1)
+
+ # 获取提现金额
+ tixian_jine = withdrawal.jine # 假设提现记录模型中有jine字段
+
+ update_daily_payout(tixian_jine)
+
+ # 更新收支记录
+ szjilu_record.zongsy -= tixian_jine # 总收益减去提现金额
+ szjilu_record.zongzc += tixian_jine # 总支出加上提现金额
+ szjilu_record.jrzc += tixian_jine # 今日支出加上提现金额
+ szjilu_record.update_time = timezone.now()
+ szjilu_record.save()
+
+ except Szjilu.DoesNotExist:
+ # 如果收支记录表不存在id=1的记录,创建一个
+ szjilu_record = Szjilu.objects.create(
+ id=1,
+ zongsy=-tixian_jine if result == 2 else 0,
+ zongzc=tixian_jine if result == 2 else 0,
+ jrzc=tixian_jine if result == 2 else 0
+ )
+ except Exception as e:
+ # 如果更新收支记录失败,抛出异常,事务会回滚
+ raise Exception(f"更新收支记录失败: {str(e)}")
+
+ return Response({
+ 'code': 0,
+ 'message': '处理成功',
+ 'data': {
+ 'id': withdrawal.id,
+ 'zhuangtai': withdrawal.zhuangtai,
+ 'shenheid': withdrawal.shenheid,
+ 'update_time': withdrawal.update_time
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ return Response({
+ 'code': 500,
+ 'message': f'处理提现失败: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+# views.py (yonghu应用)
+class DashouZiliaoHuoquView(APIView):
+ """
+ 打手资料获取接口
+ URL: /yonghu/dszjxxhq
+ 方法: POST
+ 认证: JWT Token
+ 返回: 打手扩展表信息
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 使用select_related高效查询,避免N+1问题
+ dashou = UserDashou.objects.select_related('user').get(user=request.user)
+
+ # 构建返回数据
+ data = {
+ 'nicheng': dashou.nicheng if dashou.nicheng else '',
+ 'jieshao': dashou.jieshao if dashou.jieshao else '',
+ 'dianhua': dashou.dianhua if dashou.dianhua else '',
+ 'wechat': dashou.wechat if dashou.wechat else '',
+ 'uid': request.user.yonghuid, # 从主表获取UID
+ 'avatar': request.user.avatar if request.user.avatar else '', # 头像相对URL
+ }
+
+ return Response({
+ 'code': 200,
+ 'msg': 'success',
+ 'data': data
+ }, status=status.HTTP_200_OK)
+
+ except UserDashou.DoesNotExist:
+ logger.warning(f'打手扩展表不存在: user_id={request.user.id}')
+ return Response({
+ 'code': 404,
+ 'msg': '打手信息不存在'
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ except Exception as e:
+ logger.error(f'获取打手资料异常: {str(e)}', exc_info=True)
+ return Response({
+ 'code': 500,
+ 'msg': '服务器内部错误'
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+# views.py (yonghu应用)
+
+class DashouGengxinView(APIView):
+ """
+ 打手信息更新接口
+ URL: /yonghu/dsgxxx
+ 方法: POST
+ 认证: JWT Token
+ 参数: nicheng(昵称), dianhua(电话), wechat(微信), jieshao(介绍)
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 获取请求数据
+ nicheng = request.data.get('nicheng', '').strip()
+ dianhua = request.data.get('dianhua', '').strip()
+ wechat = request.data.get('wechat', '').strip()
+ jieshao = request.data.get('jieshao', '').strip()
+
+ # 数据验证
+ validation_errors = self.validate_data(nicheng, dianhua, wechat, jieshao)
+ if validation_errors:
+ return Response({
+ 'code': 400,
+ 'msg': validation_errors
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取打手扩展表
+ try:
+ dashou = UserDashou.objects.get(user=request.user)
+ except UserDashou.DoesNotExist:
+ return Response({
+ 'code': 404,
+ 'msg': '打手信息不存在'
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 更新数据
+ update_fields = []
+
+ if nicheng is not None:
+ dashou.nicheng = nicheng if nicheng != '' else None
+ update_fields.append('nicheng')
+
+ if dianhua is not None:
+ dashou.dianhua = dianhua if dianhua != '' else None
+ update_fields.append('dianhua')
+
+ if wechat is not None:
+ dashou.wechat = wechat if wechat != '' else None
+ update_fields.append('wechat')
+
+ if jieshao is not None:
+ dashou.jieshao = jieshao if jieshao != '' else None
+ update_fields.append('jieshao')
+
+ # 保存更新(只更新修改的字段)
+ if update_fields:
+ dashou.save(update_fields=update_fields)
+
+ return Response({
+ 'code': 200,
+ 'msg': '更新成功',
+ 'data': {
+ 'nicheng': dashou.nicheng or '',
+ 'dianhua': dashou.dianhua or '',
+ 'wechat': dashou.wechat or '',
+ 'jieshao': dashou.jieshao or ''
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f'更新打手信息异常: {str(e)}', exc_info=True)
+ return Response({
+ 'code': 500,
+ 'msg': '服务器内部错误'
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def validate_data(self, nicheng, dianhua, wechat, jieshao):
+ """数据验证方法"""
+ errors = []
+
+ # 1. 昵称验证(必填)
+ if not nicheng:
+ errors.append('昵称不能为空')
+ elif len(nicheng) > 20:
+ errors.append('昵称不能超过20个字符')
+
+ # 2. 手机号验证(非必填,但填写时必须正确)
+ if dianhua:
+ # 手机号正则:1开头,11位数字
+ phone_pattern = r'^1[3-9]\d{9}$'
+ if not re.match(phone_pattern, dianhua):
+ errors.append('手机号格式不正确')
+
+ # 3. 微信号验证(非必填)
+ if wechat and len(wechat) > 30:
+ errors.append('微信号不能超过30个字符')
+
+ # 4. 个人介绍验证
+ if jieshao and len(jieshao) > 200:
+ errors.append('个人介绍不能超过200个字符')
+
+ return '; '.join(errors) if errors else None
+
+
+
+
+
+
+
+
+class ShangjiaZhuceView(APIView):
+ """
+ 商家注册接口 (通过管理员邀请码)
+ 访问路径: /api/yonghu/shangjiahuce/
+ 请求方法: POST
+ 权限要求: JWT Token认证通过的用户
+ 请求体: { "inviteCode": "管理员邀请码" }
+
+ 返回字段说明:
+ - code: 状态码(200成功,其他失败)
+ - message: 提示信息
+ - data: 商家信息数据,包含以下字段(与前端商家页面完全一致):
+ - nicheng: 商家昵称
+ - sjyue: 商家余额(字符串,保留两位小数)
+ - fabu: 发布订单总数
+ - chengjiao: 成交订单总数
+ - tuikuan: 退款订单总数
+ - jinridingdan: 今日订单
+ - jinriliushui: 今日流水(字符串)
+ - jinyuedingdan: 今月订单
+ - jinyueliushui: 今月流水(字符串)
+ - zhuangtai: 商家状态(1正常)
+ - dianhua: 商家电话
+ - wechat: 商家微信
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 获取并验证邀请码参数
+ yaoqingma = request.data.get('inviteCode')
+ if not yaoqingma:
+ return Response({
+ 'code': 400,
+ 'message': '邀请码不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ yaoqingma = yaoqingma.strip()
+ if len(yaoqingma) > 100:
+ return Response({
+ 'code': 400,
+ 'message': '邀请码长度超过限制',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ current_user = request.user
+
+ # 2. 检查用户是否已是商家
+ try:
+ shangjia_profile = current_user.shop_profile
+ # 用户已是商家,直接返回现有信息
+ return self.fanhuiXianyouShangjiaXinxi(shangjia_profile, current_user)
+ except UserShangjia.DoesNotExist:
+ # 用户不是商家,继续注册流程
+ pass
+
+ # 3. 根据邀请码查找对应的管理员
+ try:
+ # 从管理员表查找邀请码
+ admin_profile = AdminProfile.objects.select_related('user').get(yaoqingma=yaoqingma)
+ except AdminProfile.DoesNotExist:
+ return Response({
+ 'code': 404,
+ 'message': '管理员邀请码无效或不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 核心:在数据库事务中创建商家身份
+ try:
+ with transaction.atomic():
+ # 4.1 查询老板扩展表获取老板昵称
+ boss_nickname = '普通商家'
+ try:
+ boss_profile = current_user.boss_profile
+ if boss_profile.nickname and boss_profile.nickname.strip():
+ boss_nickname = boss_profile.nickname.strip()
+ except UserBoss.DoesNotExist:
+ # 老板扩展表不存在,使用默认值
+ pass
+
+ # 4.2 创建商家扩展表
+ # 使用老板昵称作为商家昵称的基础
+ shangjia_nicheng = boss_nickname
+
+ # 检查昵称是否已存在(安全考虑)
+ if UserShangjia.objects.filter(nicheng=shangjia_nicheng).exists():
+ # 如果存在,添加随机数确保唯一
+ shangjia_nicheng = f"{boss_nickname}{random.randint(1000, 9999)}"
+
+ # 4.3 创建商家记录
+ shop_profile = UserShangjia.objects.create(
+ user=current_user,
+ nicheng=shangjia_nicheng,
+ zhuangtai=1, # 正常状态
+ yue=0.00, # 商家余额
+ fabu=0, # 发布订单总数
+ tuikuan=0, # 退款订单总数
+ chengjiao=0, # 成交订单总数
+ jinridingdan=0, # 今日订单
+ jinriliushui=0.00, # 今日流水
+ jinyuedingdan=0, # 今月订单
+ jinyueliushui=0.00, # 今月流水
+ dianhua='', # 电话
+ wechat='', # 微信
+ )
+
+ except Exception as e:
+ # 事务会自动回滚
+ error_detail = traceback.format_exc()
+ #print(f"商家注册错误详情: {error_detail}")
+
+ return Response({
+ 'code': 500,
+ 'message': f'商家注册过程中发生系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 5. 注册成功,返回商家信息
+ fanhui_data = self.zhuangbeiFanhuiShuju(shop_profile, current_user)
+ return Response({
+ 'code': 200,
+ 'message': '商家注册成功!',
+ 'data': fanhui_data
+ })
+
+ def fanhuiXianyouShangjiaXinxi(self, shangjia_profile, current_user):
+ """处理已是商家的用户,返回其现有信息"""
+ fanhui_data = self.zhuangbeiFanhuiShuju(shangjia_profile, current_user)
+ return Response({
+ 'code': 200,
+ 'message': '您已是商家,返回当前信息。',
+ 'data': fanhui_data
+ })
+
+ def zhuangbeiFanhuiShuju(self, shangjia_profile, current_user):
+ """
+ 准备返回给前端的商家数据
+ 字段名必须与前端商家页面完全一致
+ """
+ data = {
+ # 🟢 商家基本信息
+ 'nicheng': shangjia_profile.nicheng, # 商家昵称
+ 'sjyue': format(shangjia_profile.yue, '.2f'), # 商家余额,保留两位小数
+ 'fabu': shangjia_profile.fabu, # 发布订单总数
+ 'chengjiao': shangjia_profile.chengjiao, # 成交订单总数
+ 'tuikuan': shangjia_profile.tuikuan, # 退款订单总数
+ 'jinridingdan': shangjia_profile.jinridingdan, # 今日订单
+ 'jinriliushui': format(shangjia_profile.jinriliushui, '.2f'), # 今日流水
+ 'jinyuedingdan': shangjia_profile.jinyuedingdan, # 今月订单
+ 'jinyueliushui': format(shangjia_profile.jinyueliushui, '.2f'), # 今月流水
+ 'zhuangtai': shangjia_profile.zhuangtai, # 商家状态
+ 'dianhua': shangjia_profile.dianhua or '', # 电话
+ 'wechat': shangjia_profile.wechat or '', # 微信
+
+ # 🟢 可能需要的主表信息
+ 'uid': current_user.yonghuid, # 用户ID
+ }
+ return data
+
+
+
+
+
+
+
+# yonghu/ranking_views.py
+"""
+排行榜查询接口 - 管理员专用
+安全认证:JWT Token + 管理员权限验证
+"""
+
+# yonghu/ranking_views.py - 修改PaihangbangGuanliQueryView的post方法参数接收
+class PaihangbangGuanliQueryView(APIView):
+ """
+ 排行榜查询接口 - 管理员专用
+ 安全要求:
+ 1. JWT Token认证
+ 2. 管理员权限验证
+ 3. 参数严格验证
+ 4. 返回统一格式
+ """
+
+ # JWT Token认证
+ permission_classes = [IsAuthenticated]
+
+ @admin_required
+ def post(self, request):
+ """
+ POST请求查询排行榜数据
+ 参数:
+ - zhanghao: 管理员账号
+ - shenfen: 用户身份(必须,2=打手,3=管事,4=商家)
+ - zhouqi: 周期类型(必须,1=日榜,2=月榜)
+ - date: 查询日期(必须,格式:YYYY-MM-DD)
+
+ 返回格式:
+ {
+ "code": 0, # 0=成功,非0=失败
+ "message": "成功",
+ "data": {
+ "list": [...], # 排行榜数据列表
+ "query_info": {...} # 查询信息
+ }
+ }
+ """
+ try:
+ # 1. 获取请求数据
+ data = request.data
+ zhanghao = data.get('zhanghao')
+ shenfen = data.get('shenfen')
+ zhouqi = data.get('zhouqi')
+ date_str = data.get('date')
+
+ # 2. 参数验证
+ if not zhanghao:
+ return Response({
+ 'code': 400,
+ 'message': '管理员账号不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not shenfen:
+ return Response({
+ 'code': 400,
+ 'message': '用户身份不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not zhouqi:
+ return Response({
+ 'code': 400,
+ 'message': '周期类型不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not date_str:
+ return Response({
+ 'code': 400,
+ 'message': '查询日期不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 参数类型和范围验证
+ try:
+ zhouqi_int = int(zhouqi)
+ shenfen_int = int(shenfen)
+ date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
+ except ValueError as e:
+ return Response({
+ 'code': 400,
+ 'message': f'参数格式错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证周期类型范围
+ if zhouqi_int not in [1, 2]:
+ return Response({
+ 'code': 400,
+ 'message': '周期类型必须为1(日榜)或2(月榜)',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证用户身份范围
+ if shenfen_int not in [2, 3, 4]:
+ return Response({
+ 'code': 400,
+ 'message': '用户身份必须为2(打手)、3(管事)或4(商家)',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 构建查询条件
+ query = Q(
+ zhouqi=zhouqi_int,
+ tongji_date=date_obj,
+ shenfen=shenfen_int
+ )
+
+ # 5. 执行查询(按排名升序)
+ records = RankingRecord.objects.filter(query).order_by('paiming')
+
+ # 6. 格式化返回数据
+ data_list = []
+ for record in records:
+ # 根据用户身份确定指标值单位
+ zhizhi_danwei = self._get_zhizhi_danwei(shenfen_int)
+
+ data_list.append({
+ 'paiming': record.paiming,
+ 'yonghuid': record.yonghuid,
+ 'nicheng': record.nicheng,
+ 'avatar': record.avatar or '',
+ 'zhizhi': float(record.zhizhi),
+ 'zhizhi_danwei': zhizhi_danwei,
+ })
+
+ # 7. 返回成功响应
+ response_data = {
+ 'code': 0,
+ 'message': '查询成功',
+ 'data': {
+ 'list': data_list,
+ 'total_count': len(data_list),
+ 'query_info': {
+ 'zhouqi': zhouqi_int,
+ 'zhouqi_text': '日榜' if zhouqi_int == 1 else '月榜',
+ 'shenfen': shenfen_int,
+ 'shenfen_text': self._get_shenfen_text(shenfen_int),
+ 'date': date_str,
+ }
+ }
+ }
+
+ return Response(response_data, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f"管理员查询排行榜失败: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 500,
+ 'message': f'查询排行榜失败: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def _get_shenfen_text(self, shenfen):
+ """获取身份文本"""
+ shenfen_map = {2: '打手', 3: '管事', 4: '商家'}
+ return shenfen_map.get(shenfen, '未知')
+
+ def _get_zhizhi_danwei(self, shenfen):
+ """获取指标值单位"""
+ danwei_map = {2: '元', 3: '人', 4: '元'}
+ return danwei_map.get(shenfen, '')
+
+
+
+
+
+
+
+# yonghu/ranking_views.py - 添加以下代码
+class PaihangbangRiqiliebiaoView(APIView):
+ """
+ 获取排行榜可查询日期列表 - 管理员专用
+ 返回当前排行榜表中存在的日期(日榜和月榜分开)
+ """
+
+ # JWT Token认证
+ permission_classes = [IsAuthenticated]
+
+ @admin_required
+ def post(self, request):
+ """
+ POST请求获取可查询日期列表
+ 参数:
+ - zhanghao: 管理员账号(用于验证)
+
+ 返回格式:
+ {
+ "code": 0,
+ "message": "成功",
+ "data": {
+ "ribang_dates": ["2024-01-15", "2024-01-14", ...], # 日榜日期列表
+ "yuebang_dates": ["2024-01-01", "2023-12-01", ...], # 月榜日期列表
+ }
+ }
+ """
+ try:
+ # 1. 获取请求数据
+ data = request.data
+ zhanghao = data.get('zhanghao')
+
+ if not zhanghao:
+ return Response({
+ 'code': 400,
+ 'message': '管理员账号不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 查询日榜日期(最近3个月内的)
+ three_months_ago = timezone.now().date() - timedelta(days=90)
+
+ ribang_dates = RankingRecord.objects.filter(
+ zhouqi=1, # 日榜
+ tongji_date__gte=three_months_ago # 只查询最近3个月
+ ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date')
+
+ # 3. 查询月榜日期(最近12个月内的)
+ twelve_months_ago = timezone.now().date() - timedelta(days=365)
+
+ yuebang_dates = RankingRecord.objects.filter(
+ zhouqi=2, # 月榜
+ tongji_date__gte=twelve_months_ago # 只查询最近12个月
+ ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date')
+
+ # 4. 格式化日期
+ ribang_date_list = [date.strftime('%Y-%m-%d') for date in ribang_dates]
+ yuebang_date_list = [date.strftime('%Y-%m-%d') for date in yuebang_dates]
+
+ # 5. 返回成功响应
+ response_data = {
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': {
+ 'ribang_dates': ribang_date_list,
+ 'yuebang_dates': yuebang_date_list,
+ 'ribang_count': len(ribang_date_list),
+ 'yuebang_count': len(yuebang_date_list),
+ }
+ }
+
+ return Response(response_data, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f"获取排行榜日期列表失败: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 500,
+ 'message': f'获取日期列表失败: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
+
+
+
+class WechatLoginAndDashouRegisterView(APIView):
+ """
+ 微信登录并注册打手一体化接口
+ 访问路径: /api/yonghu/wdlyhdl
+ 请求方法: POST
+ 权限要求: 无需登录
+ 请求体: { "code": "微信登录code", "inviteCode": "邀请码" }
+
+ 功能: 用户扫码进入但未登录时,一次性完成微信登录和打手注册
+ """
+
+ throttle_classes = [AnonRateThrottle]
+ permission_classes = [AllowAny]
+
+ def post(self, request):
+ """
+ 处理微信登录并注册打手请求
+ """
+ try:
+ # 1. 获取并验证参数
+ code = request.data.get('code', '').strip()
+ yaoqingma = request.data.get('inviteCode', '').strip()
+
+ if not code:
+ return Response({
+ 'code': 1,
+ 'msg': '微信授权码不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not yaoqingma:
+ return Response({
+ 'code': 2,
+ 'msg': '邀请码不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if len(yaoqingma) > 100:
+ return Response({
+ 'code': 3,
+ 'msg': '邀请码长度超过限制',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 获取微信openid
+ wechat_data = self.get_wechat_openid(code)
+ if not wechat_data or 'openid' not in wechat_data:
+ error_msg = wechat_data.get('errmsg', '微信授权失败')
+ return Response({
+ 'code': 4,
+ 'msg': f'微信登录失败: {error_msg}',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ openid = wechat_data['openid']
+
+ # 3. 获取客户端真实IP
+ kehuduan_ip = self.huoquKehuduanIP(request)
+
+ # 4. 开始数据库事务(确保登录和注册的原子性)
+ with transaction.atomic():
+ # 4.1 创建或获取用户(登录逻辑)
+ user_main, created = UserMain.objects.select_for_update().get_or_create(
+ openid=openid,
+ defaults={
+ 'yonghuid': self.shengchengYonghuID(),
+ 'user_type': 'normal',
+ }
+ )
+
+ # 更新用户IP和最后登录时间
+ cunchu_ip = kehuduan_ip
+ user_main.ip = cunchu_ip
+ user_main.last_login_time = timezone.now()
+ user_main.save()
+
+ if created:
+ # 新用户:创建老板扩展表
+ UserBoss.objects.create(user=user_main, nickname='微信用户')
+
+ # 4.2 验证邀请码对应的管事(注册逻辑)
+ try:
+ # 使用select_related获取管事及其关联的主表用户信息
+ guanshi_profile = UserGuanshi.objects.select_related('user').get(yaoqingma=yaoqingma)
+ except UserGuanshi.DoesNotExist:
+ # 邀请码无效,但用户已登录,可以返回登录成功但注册失败
+ # 这里我们继续执行,但只返回登录信息
+ return self.fanhuiZhihouDengluXinxi(user_main, yaoqingma_wuxiao=True)
+
+ # 验证管事状态
+
+ # 4.3 检查用户是否已是打手
+ dashou_exists = UserDashou.objects.filter(user=user_main).exists()
+ if dashou_exists:
+ # 用户已是打手,直接返回信息
+ dashou_profile = UserDashou.objects.get(user=user_main)
+ return self.fanhuiDengluZhuceshuju(user_main, dashou_profile)
+
+ # 4.4 核心:为用户创建所有身份(打手、商家、管事)
+ guanshi_yonghuid = guanshi_profile.user.yonghuid
+
+ # 创建打手扩展表
+ dashou_profile = UserDashou.objects.create(
+ user=user_main,
+ nicheng='大神',
+ chenghao='普通大神',
+ yaoqingren=guanshi_yonghuid,
+ # 其他字段使用模型默认值
+ )
+
+ # 创建商家扩展表
+
+ # 创建管事扩展表
+
+ # 更新原管事的邀请数量
+ locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
+ locked_guanshi.yaogingshuliang += 1
+ locked_guanshi.save()
+
+ # ========== 新增:管事每日统计(邀请打手) ==========
+ try:
+ update_guanshi_daily_by_action(
+ yonghuid=guanshi_yonghuid,
+ action=1, # 1 = 邀请打手
+ amount=Decimal('0.00')
+ )
+ logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}")
+ except Exception as e:
+ logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}")
+
+ # 5. 生成JWT token
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+
+ # 6. 准备返回数据
+ response_data = self.zhuangbeiFanhuiShuju(user_main, dashou_profile, token)
+
+ return Response({
+ 'code': 0,
+ 'msg': '登录并注册成功!已开通打手、管事等身份。',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ # 记录错误日志
+ error_openid = locals().get('openid', 'N/A')
+ logger.error(
+ f"微信登录注册一体化接口异常 - openid: {error_openid}, "
+ f"错误类型: {type(e).__name__}, 错误详情: {str(e)}",
+ exc_info=True
+ )
+
+ # 返回友好错误信息
+ return Response({
+ 'code': 99,
+ 'msg': '系统繁忙,请稍后重试',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def get_wechat_openid(self, code):
+ """
+ 调用微信接口获取openid
+ """
+ try:
+ appid = getattr(settings, 'WEIXIN_APPID', '')
+ secret = getattr(settings, 'WEIXIN_SECRET', '')
+
+ if not appid or not secret:
+ raise ValueError('微信配置未设置')
+
+ url = 'https://api.weixin.qq.com/sns/jscode2session'
+ params = {
+ 'appid': appid,
+ 'secret': secret,
+ 'js_code': code,
+ 'grant_type': 'authorization_code'
+ }
+
+ response = requests.get(url, params=params, timeout=10)
+ result = response.json()
+
+ if 'openid' in result:
+ return result
+ else:
+ errcode = result.get('errcode', 'unknown')
+ errmsg = result.get('errmsg', '未知错误')
+ return {'errmsg': f'[{errcode}]{errmsg}'}
+
+ except requests.exceptions.Timeout:
+ return {'errmsg': '请求微信接口超时'}
+ except Exception as e:
+ return {'errmsg': f'请求微信接口异常: {str(e)}'}
+
+ def shengchengYonghuID(self):
+ """
+ 生成7位数字用户ID
+ """
+ for _ in range(10):
+ timestamp_part = str(int(time.time()))[-5:].zfill(5)
+ random_part = str(random.randint(0, 99)).zfill(2)
+ user_id = timestamp_part + random_part
+
+ if len(user_id) == 7 and user_id.isdigit():
+ if not UserMain.objects.filter(yonghuid=user_id).exists():
+ return user_id
+
+ raise Exception('生成用户ID失败')
+
+ def huoquKehuduanIP(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
+
+ ip = request.META.get('REMOTE_ADDR', '')
+ return ip if ip else '0.0.0.0'
+
+ def huoquQunPeizhi(self):
+ """
+ 获取群配置信息
+ """
+ try:
+ qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2])
+
+ result = {
+ 'dashouqun': '',
+ 'dashouqunid': '',
+ 'guanshiqun': '',
+ 'guanshiqunid': ''
+ }
+
+ for config in qun_configs:
+ if config.id == 1:
+ result['dashouqun'] = config.neirong or ''
+ result['dashouqunid'] = config.qunid or ''
+ elif config.id == 2:
+ result['guanshiqun'] = config.neirong or ''
+ result['guanshiqunid'] = config.qunid or ''
+
+ return result
+
+ except Exception:
+ return {
+ 'dashouqun': '',
+ 'dashouqunid': '',
+ 'guanshiqun': '',
+ 'guanshiqunid': ''
+ }
+
+ def fanhuiZhihouDengluXinxi(self, user_main, yaoqingma_wuxiao=False, guanshi_jinyong=False):
+ """
+ 返回只登录成功的信息(当邀请码无效或管事禁用时)
+ """
+ # 获取老板昵称
+ boss_nickname = '微信用户'
+ try:
+ boss_profile = UserBoss.objects.get(user=user_main)
+ if boss_profile.nickname:
+ boss_nickname = boss_profile.nickname
+ except UserBoss.DoesNotExist:
+ pass
+
+ # 检查各身份状态
+ dashou_status = 1 if UserDashou.objects.filter(user=user_main).exists() else 0
+ shangjia_status = 1 if UserShangjia.objects.filter(user=user_main).exists() else 0
+ guanshi_status = 1 if UserGuanshi.objects.filter(user=user_main).exists() else 0
+
+ # 生成token
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+
+ # 获取群配置
+ group_info = self.huoquQunPeizhi()
+
+ # 构建返回数据
+ response_data = {
+ 'token': token,
+ 'nicheng': boss_nickname,
+ 'uid': user_main.yonghuid,
+ 'touxiang': user_main.avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ # 🟢 注意:这里没有返回dingdantiaoshu(订单数量),与前端需求一致
+ 'dashouqun': group_info.get('dashouqun', ''),
+ 'dashouqunid': group_info.get('dashouqunid', ''),
+ 'guanshiqun': group_info.get('guanshiqun', ''),
+ 'guanshiqunid': group_info.get('guanshiqunid', '')
+ }
+
+ # 根据情况设置返回消息
+ if yaoqingma_wuxiao:
+ msg = '登录成功,但邀请码无效'
+ elif guanshi_jinyong:
+ msg = '登录成功,但邀请码对应的管事账号已被禁用'
+ else:
+ msg = '登录成功'
+
+ return Response({
+ 'code': 0,
+ 'msg': msg,
+ 'data': response_data
+ })
+
+ def fanhuiDengluZhuceshuju(self, user_main, dashou_profile):
+ """
+ 返回登录并注册的数据(用户已是打手的情况)
+ """
+ # 获取老板昵称
+ boss_nickname = '微信用户'
+ try:
+ boss_profile = UserBoss.objects.get(user=user_main)
+ if boss_profile.nickname:
+ boss_nickname = boss_profile.nickname
+ except UserBoss.DoesNotExist:
+ pass
+
+ # 检查各身份状态(这里用户已是打手,所以都是1)
+ dashou_status = 1
+ shangjia_status = 1 if UserShangjia.objects.filter(user=user_main).exists() else 1 # 确保是1
+ guanshi_status = 1 if UserGuanshi.objects.filter(user=user_main).exists() else 1 # 确保是1
+
+ # 生成token
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+
+ # 获取群配置
+ group_info = self.huoquQunPeizhi()
+
+ # 准备打手信息
+ dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile)
+
+ # 合并所有数据
+ response_data = {
+ 'token': token,
+ 'nicheng': boss_nickname,
+ 'uid': user_main.yonghuid,
+ 'touxiang': user_main.avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ 'dashouqun': group_info.get('dashouqun', ''),
+ 'dashouqunid': group_info.get('dashouqunid', ''),
+ 'guanshiqun': group_info.get('guanshiqun', ''),
+ 'guanshiqunid': group_info.get('guanshiqunid', ''),
+ # 合并打手信息
+ **dashou_data
+ }
+
+ return Response({
+ 'code': 0,
+ 'msg': '您已是打手,登录成功',
+ 'data': response_data
+ })
+
+ def zhuangbeiFanhuiShuju(self, user_main, dashou_profile, token):
+ """
+ 准备完整的返回数据(登录+注册成功的情况)
+ """
+ # 获取老板昵称
+ boss_nickname = '微信用户'
+ try:
+ boss_profile = UserBoss.objects.get(user=user_main)
+ if boss_profile.nickname:
+ boss_nickname = boss_profile.nickname
+ except UserBoss.DoesNotExist:
+ pass
+
+ # 身份状态(刚注册成功,应该都是1)
+ dashou_status = 1
+ shangjia_status = 1
+ guanshi_status = 1
+
+ # 获取群配置
+ group_info = self.huoquQunPeizhi()
+
+ # 准备打手信息
+ dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile)
+
+ # 合并所有数据
+ response_data = {
+ 'token': token,
+ 'nicheng': boss_nickname,
+ 'uid': user_main.yonghuid,
+ 'touxiang': user_main.avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ 'dashouqun': group_info.get('dashouqun', ''),
+ 'dashouqunid': group_info.get('dashouqunid', ''),
+ 'guanshiqun': group_info.get('guanshiqun', ''),
+ 'guanshiqunid': group_info.get('guanshiqunid', ''),
+ # 合并打手信息
+ **dashou_data
+ }
+
+ return response_data
+
+ def zhuangbeiDashouFanhuiShuju(self, dashou_profile):
+ """
+ 准备打手返回数据(字段名严格匹配前端)
+ """
+ # 构建基础信息
+ data = {
+ 'dashounicheng': dashou_profile.nicheng,
+ 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
+ 'yongjin': str(dashou_profile.yue), # Decimal转字符串
+ 'zonge': str(dashou_profile.zonge),
+ 'yajin': str(dashou_profile.yajin),
+ 'chenghao': dashou_profile.chenghao,
+ 'jinfen': dashou_profile.jifen,
+ 'chengjiaoliang': dashou_profile.chengjiaozongliang,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'dashouzhuangtai': dashou_profile.zhuangtai,
+ }
+
+ # 查询并处理会员列表 (clumber)
+ # 使用当前用户的yonghuid进行查询
+ huiyuan_goumai_list = []
+ goumai_records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid)
+ for record in goumai_records:
+ # 调用方法检查是否到期,此方法内部会更新状态
+ shifou_daoqi = record.jiance_shifou_daoqi()
+ huiyuan_goumai_list.append({
+ 'huiyuan_id': record.huiyuan_id,
+ 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
+ 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
+ 'shifou_daoqi': shifou_daoqi,
+ 'jieshao': record.jieshao
+ })
+
+ data['clumber'] = huiyuan_goumai_list
+ return data
+
+class YonghuTixianShenheXiugaiView(APIView):
+ """
+ 用户修改审核中提现申请接口
+ 访问路径: /api/yonghu/yonghutxshxg
+ 请求方法: POST
+ 权限要求: JWT Token认证通过的用户
+ 请求体: multipart/form-data 格式
+ 支持字段: tixian_id, fangshi, txdianhua, txzh, file (图片文件)
+
+ 功能: 允许用户修改审核中提现申请的收款方式、电话、账号、收款码等
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = (MultiPartParser, FormParser, JSONParser)
+
+ def post(self, request):
+ """
+ 处理用户修改审核中提现申请的POST请求
+ """
+ try:
+ # 1. 获取当前用户
+ user_main = request.user
+ current_yonghuid = user_main.yonghuid
+
+ # 2. 验证必填字段:提现记录ID
+ tixian_id = request.data.get('tixian_id')
+ if not tixian_id:
+ return Response({
+ 'code': 1,
+ 'msg': '提现记录ID不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 查询提现记录(使用select_for_update确保事务安全)
+ with transaction.atomic():
+ try:
+ tixian_record = Tixianjilu.objects.select_for_update().get(
+ id=tixian_id,
+ yonghuid=current_yonghuid # 确保只能修改自己的记录
+ )
+ except Tixianjilu.DoesNotExist:
+ return Response({
+ 'code': 2,
+ 'msg': '提现记录不存在或无权修改',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 验证提现记录状态(必须是审核中)
+ if tixian_record.zhuangtai != 1:
+ return Response({
+ 'code': 3,
+ 'msg': '只有审核中的提现申请可以修改',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 获取并验证提现方式
+ fangshi = request.data.get('fangshi')
+ if fangshi:
+ fangshi = int(fangshi)
+ if fangshi not in [1, 2]:
+ return Response({
+ 'code': 4,
+ 'msg': '提现方式无效,请选择1(微信)或2(支付宝)',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 检查提现类型与方式的匹配(如果前端提供了leixing)
+ leixing = request.data.get('leixing')
+ if leixing:
+ leixing = int(leixing)
+ # 这里可以根据业务逻辑添加类型与方式的验证
+ # 例如:某些类型只能使用某些方式
+
+ # 6. 验证手机号(必填)
+ txdianhua = request.data.get('txdianhua', '').strip()
+ if not txdianhua:
+ return Response({
+ 'code': 5,
+ 'msg': '收款人电话不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证手机号格式
+ if not txdianhua.isdigit() or len(txdianhua) != 11:
+ return Response({
+ 'code': 6,
+ 'msg': '请输入11位有效的手机号码',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 7. 获取收款账号(可选)
+ txzh = request.data.get('txzh', '').strip()
+
+ # 8. 处理收款码图片上传(如果有)
+ txtupian_file = request.FILES.get('file')
+ new_zhifu_url = None
+ old_zhifu_url = tixian_record.zhifu
+
+ if txtupian_file:
+ # 验证图片
+ is_valid, error_msg = validate_image(txtupian_file)
+ if not is_valid:
+ return Response({
+ 'code': 7,
+ 'msg': f'图片验证失败: {error_msg}',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 上传收款码图片
+ new_zhifu_url = self.handle_shoukuanma_upload(
+ user_main,
+ txtupian_file,
+ old_zhifu_url,
+ tixian_record.leixing
+ )
+
+ if not new_zhifu_url:
+ return Response({
+ 'code': 8,
+ 'msg': '收款码上传失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 9. 验证收款信息(账号和图片至少有一个)
+ if not txzh and not (new_zhifu_url or old_zhifu_url):
+ return Response({
+ 'code': 9,
+ 'msg': '请填写收款账号或上传收款码',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 10. 更新提现记录
+ update_fields = []
+
+ # 更新提现方式(如果有)
+ if fangshi:
+ tixian_record.fangshi = fangshi
+ update_fields.append('fangshi')
+
+ # 更新手机号(必填)
+ tixian_record.phone = txdianhua
+ update_fields.append('phone')
+
+ # 更新收款账号(如果有)
+ if txzh is not None: # 注意:空字符串也要更新
+ tixian_record.skzhanghao = txzh
+ update_fields.append('skzhanghao')
+
+ # 更新收款码图片(如果有新图片)
+ if new_zhifu_url:
+ tixian_record.zhifu = new_zhifu_url
+ update_fields.append('zhifu')
+
+ # 保存更新
+ if update_fields:
+ tixian_record.save(update_fields=update_fields)
+
+ # 11. 构建返回数据
+ response_data = {
+ 'tixian_id': tixian_record.id,
+ 'fangshi': tixian_record.fangshi,
+ 'txdianhua': tixian_record.phone,
+ 'txzh': tixian_record.skzhanghao or '',
+ 'txtupian': tixian_record.zhifu or '' # 返回相对URL
+ }
+
+ # 12. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '提现申请修改成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ # 记录详细错误日志
+ logger.error(
+ f"修改提现申请异常 - 用户ID: {getattr(user_main, 'yonghuid', 'N/A')}, "
+ f"提现ID: {tixian_id}, "
+ f"错误类型: {type(e).__name__}, "
+ f"错误详情: {str(e)}",
+ exc_info=True
+ )
+
+ # 返回友好错误信息
+ return Response({
+ 'code': 99,
+ 'msg': '系统繁忙,请稍后重试',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def handle_shoukuanma_upload(self, user_main, image_file, old_zhifu_url, leixing=None):
+ """
+ 处理收款码图片上传到腾讯云COS
+ 存储路径: tixian/{yonghuid}/{timestamp_uuid}.扩展名
+
+ 参数:
+ - user_main: 当前用户对象
+ - image_file: 图片文件对象
+ - old_zhifu_url: 旧收款码URL(用于删除)
+ - leixing: 提现类型(可选,用于文件夹分类)
+
+ 返回: 新的图片相对路径
+ """
+ try:
+ yonghuid = user_main.yonghuid
+ if not yonghuid:
+ return None
+
+ # 删除旧收款码(如果存在且不是空字符串)
+ if old_zhifu_url and old_zhifu_url.strip():
+ delete_from_oss(old_zhifu_url)
+
+ # 获取文件扩展名
+ file_name = image_file.name
+ file_extension = os.path.splitext(file_name)[1].lower()
+ if not file_extension:
+ file_extension = '.jpg'
+
+ # 生成唯一文件名(使用时间戳+UUID确保不重复)
+ timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
+ unique_id = str(uuid.uuid4())[:8] # 取UUID前8位
+ unique_file_name = f"{timestamp}_{unique_id}{file_extension}"
+
+ # 根据提现类型构建文件夹路径(如果提供了leixing)
+ if leixing:
+ # leixing=1:打手佣金, leixing=2:管事分红
+ type_folder = 'dashou' if leixing == 1 else 'guanshi'
+ oss_file_path = f"tixian/{type_folder}/{yonghuid}/{unique_file_name}"
+ else:
+ # 默认路径
+ oss_file_path = f"tixian/{yonghuid}/{unique_file_name}"
+
+ # 上传新收款码到OSS
+ new_shoukuanma_url = upload_to_oss(image_file, oss_file_path)
+
+ if new_shoukuanma_url:
+ # 返回相对路径(从tixian文件夹开始)
+ return oss_file_path
+ else:
+ return None
+
+ except Exception as e:
+ print(f"处理收款码上传失败: {e}")
+ return None
+
+
+# views.py
+class DashouPermission:
+ """
+ 打手权限验证类
+ 确保用户是打手且账号状态正常
+ """
+
+ def has_permission(self, request, view):
+ user = request.user
+
+ # 检查用户是否已认证
+ if not user.is_authenticated:
+ return False
+
+ try:
+ # 通过反向关系获取打手扩展信息
+ dashou_profile = user.dashou_profile
+
+ # 检查账号状态是否为1(正常)
+ if dashou_profile.zhanghaozhuangtai != 1:
+ return False
+
+ return True
+ except UserDashou.DoesNotExist:
+ return False
+ except Exception as e:
+ logger.error(f"打手权限验证异常: {e}")
+ return False
+
+
+
+
+class ChufaJiluHuoquView(APIView):
+ """
+ 打手获取处罚记录接口
+ 路径: /yonghu/dshqcfjl
+ 方法: POST
+ 权限: JWT Token + 打手权限验证
+ """
+ permission_classes = [IsAuthenticated, DashouPermission]
+
+ def post(self, request):
+ try:
+ # 获取当前用户信息
+ user_main = request.user
+ yonghuid = user_main.yonghuid
+
+ # 通过反向关系获取打手扩展信息
+ try:
+ dashou_profile = user_main.dashou_profile
+ # 再次确认账号状态
+ if dashou_profile.zhanghaozhuangtai != 1:
+ return Response({
+ 'code': 3,
+ 'msg': '账号已被封禁,无法查看处罚记录',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+ except UserDashou.DoesNotExist:
+ return Response({
+ 'code': 2,
+ 'msg': '用户不是打手身份',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 判断请求类型:统计信息 or 记录列表
+ qingqiu_tongji = request.data.get('qingqiu_tongji', False)
+
+ if qingqiu_tongji:
+ # 返回统计信息
+ return self.get_tongji_info(yonghuid)
+ else:
+ # 返回处罚记录列表
+ return self.get_chufa_list(yonghuid, request.data)
+
+ except Exception as e:
+ logger.error(f"获取处罚记录异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 99,
+ 'msg': '系统繁忙,请稍后重试',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def get_tongji_info(self, yonghuid):
+ """
+ 获取统计信息(总记录、待处理、已处理)
+ """
+ try:
+ # 使用单个查询获取所有统计信息
+ stats = Chufajilu.objects.filter(dashouid=yonghuid).aggregate(
+ # 总记录数
+ zongshu=Count('id'),
+
+ # 待处理数:状态为0(待处罚)和3(申诉中)
+ daichuli=Count(
+ Case(
+ When(Q(sqzhuangtai=0) | Q(sqzhuangtai=3), then=1),
+ output_field=IntegerField()
+ )
+ ),
+
+ # 已处理数:状态为1(已处罚)和2(已驳回)
+ yichuli=Count(
+ Case(
+ When(Q(sqzhuangtai=1) | Q(sqzhuangtai=2), then=1),
+ output_field=IntegerField()
+ )
+ )
+ )
+
+ # 如果查询结果为None,则设置为0
+ tongji_data = {
+ 'zongshu': stats.get('zongshu', 0) or 0,
+ 'daichuli': stats.get('daichuli', 0) or 0,
+ 'yichuli': stats.get('yichuli', 0) or 0
+ }
+
+ return Response({
+ 'code': 0,
+ 'msg': '获取统计信息成功',
+ 'data': tongji_data
+ })
+
+ except Exception as e:
+ logger.error(f"获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}")
+ return Response({
+ 'code': 1,
+ 'msg': '获取统计信息失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def get_chufa_list(self, yonghuid, request_data):
+ """
+ 获取处罚记录列表(高性能批量查询)
+ 🔴 只修改图片查询逻辑,字段保持原样
+ """
+ try:
+ # 获取分页参数
+ page = int(request_data.get('page', 1))
+ page_size = int(request_data.get('page_size', 10))
+
+ # 限制最大每页数量,防止恶意请求
+ if page_size > 50:
+ page_size = 50
+ if page_size < 1:
+ page_size = 10
+
+ # 获取状态筛选参数
+ zhuangtai = request_data.get('zhuangtai', 0)
+
+ # 🔴 构建查询条件 - 打手端根据dashouid查询
+ queryset = Chufajilu.objects.filter(dashouid=yonghuid)
+
+ if zhuangtai == 0:
+ # 待处理:状态为0(待处罚)和3(申诉中)
+ queryset = queryset.filter(Q(sqzhuangtai=0) | Q(sqzhuangtai=3))
+ elif zhuangtai == 1:
+ # 已处理:状态为1(已处罚)和2(已驳回)
+ queryset = queryset.filter(Q(sqzhuangtai=1) | Q(sqzhuangtai=2))
+ else:
+ # 默认返回所有状态
+ pass
+
+ # 按创建时间倒序排列(最新在前)
+ queryset = queryset.order_by('-create_time')
+
+ # 使用游标分页,避免传统分页的性能问题
+ paginator = Paginator(queryset, page_size)
+
+ try:
+ page_obj = paginator.page(page)
+ except:
+ # 页码超出范围,返回空列表
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': {
+ 'list': [],
+ 'has_more': False,
+ 'page': page,
+ 'page_size': page_size,
+ 'current_count': 0
+ }
+ })
+
+ # 获取当前页的记录
+ chufa_records = list(page_obj)
+
+ if not chufa_records:
+ # 没有数据直接返回
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': {
+ 'list': [],
+ 'has_more': False,
+ 'page': page,
+ 'page_size': page_size,
+ 'current_count': 0
+ }
+ })
+
+ # 🔴【核心修复】收集所有需要查询图片的组合
+ dingdan_ids = []
+ shangjia_ids = []
+
+ for record in chufa_records:
+ if record.dingdan_id:
+ dingdan_ids.append(record.dingdan_id)
+ if record.qingqiuid: # 商家ID
+ shangjia_ids.append(record.qingqiuid)
+
+ # 去重
+ dingdan_ids = list(set(dingdan_ids))
+ shangjia_ids = list(set(shangjia_ids))
+
+ # 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误)
+ # 1. 查询证据图片(商家上传的)
+ zhengju_mapping = {}
+ if dingdan_ids and shangjia_ids:
+ # 构建Q对象:dingdan_id在列表中 AND yonghuid在商家ID列表中
+ zhengju_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=shangjia_ids)
+ zhengju_queryset = Chufatupian.objects.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian')
+
+ for item in zhengju_queryset:
+ key = f"{item['dingdan_id']}_{item['yonghuid']}"
+ if key not in zhengju_mapping:
+ zhengju_mapping[key] = []
+ if item['tupian']:
+ zhengju_mapping[key].append(item['tupian'])
+
+ # 2. 查询申诉图片(打手上传的)
+ shensu_mapping = {}
+ if dingdan_ids:
+ # 构建Q对象:dingdan_id在列表中 AND yonghuid=当前打手ID
+ shensu_q = Q(dingdan_id__in=dingdan_ids, yonghuid=yonghuid)
+ shensu_queryset = Chufatupian.objects.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian')
+
+ for item in shensu_queryset:
+ key = f"{item['dingdan_id']}_{item['yonghuid']}"
+ if key not in shensu_mapping:
+ shensu_mapping[key] = []
+ if item['tupian']:
+ shensu_mapping[key].append(item['tupian'])
+
+ # 🔴【核心修复】组装返回数据 - 字段保持原样!
+ chufa_list = []
+ for record in chufa_records:
+ dingdan_id = record.dingdan_id
+ shangjia_id = record.qingqiuid
+
+ # 获取证据图片(商家上传的)
+ zhengju_tupian = []
+ if dingdan_id and shangjia_id:
+ key = f"{dingdan_id}_{shangjia_id}"
+ zhengju_tupian = zhengju_mapping.get(key, [])
+
+ # 获取申诉图片(打手上传的)
+ shensu_tupian = []
+ if dingdan_id:
+ key = f"{dingdan_id}_{yonghuid}"
+ shensu_tupian = shensu_mapping.get(key, [])
+
+ # 🔴【字段保持原样!】与前端完全对应!
+ chufa_data = {
+ 'id': record.id,
+ 'dingdan_id': record.dingdan_id or '',
+ 'qingqiuid': record.qingqiuid or '', # 商家ID
+ 'cfliyou': record.cfliyou or '',
+ 'sqzhuangtai': record.sqzhuangtai or 0,
+ 'ssliyou': record.ssliyou or '',
+ 'jifen': record.jifen or 0,
+ 'create_time': record.create_time.strftime('%Y-%m-%d %H:%M:%S') if record.create_time else '',
+ 'update_time': record.update_time.strftime('%Y-%m-%d %H:%M:%S') if record.update_time else '',
+ 'zhengju_tupian': zhengju_tupian, # 商家上传的证据图片
+ 'shensu_tupian': shensu_tupian, # 打手上传的申诉图片
+ }
+ chufa_list.append(chufa_data)
+
+ # 判断是否还有更多数据
+ has_more = page_obj.has_next()
+
+ # 构建响应数据
+ response_data = {
+ 'list': chufa_list,
+ 'has_more': has_more,
+ 'page': page,
+ 'page_size': page_size,
+ 'current_count': len(chufa_list)
+ }
+
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': response_data
+ })
+
+ except ValueError as e:
+ logger.error(f"参数错误 - 用户ID: {yonghuid}, 错误: {str(e)}")
+ return Response({
+ 'code': 4,
+ 'msg': '参数格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+ except Exception as e:
+ logger.error(f"获取处罚记录列表异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 5,
+ 'msg': '获取记录列表失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+class DashouShensuView(APIView):
+ """
+ 打手申诉接口
+ 路径: /yonghu/dscfss
+ 方法: POST
+ 权限: JWT Token + 打手权限验证
+ 功能: 打手对处罚进行申诉
+ """
+ permission_classes = [IsAuthenticated, DashouPermission]
+
+ def post(self, request):
+ try:
+ # 获取当前用户信息
+ user_main = request.user
+ yonghuid = user_main.yonghuid
+
+ # 🔴🆕【关键】验证打手账号状态
+ try:
+ dashou_profile = user_main.dashou_profile
+ if dashou_profile.zhanghaozhuangtai != 1:
+ return Response({
+ 'code': 3,
+ 'msg': '账号已被封禁,无法申诉',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+ except UserDashou.DoesNotExist:
+ return Response({
+ 'code': 2,
+ 'msg': '用户不是打手身份',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 🔴🆕【关键】获取请求参数
+ chufa_id = request.data.get('chufa_id') # 处罚记录ID
+ shensu_liyou = request.data.get('shensu_liyou', '').strip() # 申诉理由
+ shensu_tupian_urls = request.data.get('shensu_tupian_urls', []) # 申诉图片URL数组
+
+ # 验证必填字段
+ if not chufa_id:
+ return Response({
+ 'code': 6,
+ 'msg': '处罚记录ID不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not shensu_liyou:
+ return Response({
+ 'code': 7,
+ 'msg': '申诉理由不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证申诉理由长度
+ if len(shensu_liyou) > 500:
+ return Response({
+ 'code': 8,
+ 'msg': '申诉理由不能超过500字',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证图片数量
+ if len(shensu_tupian_urls) > 9:
+ return Response({
+ 'code': 9,
+ 'msg': '最多只能上传9张图片',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 🔴🆕【关键】使用事务确保数据一致性
+
+ with transaction.atomic():
+ # 1. 查询处罚记录
+ try:
+ chufa_record = Chufajilu.objects.select_for_update().get(
+ id=chufa_id,
+ dashouid=yonghuid # 确保只能申诉自己的处罚记录
+ )
+ except Chufajilu.DoesNotExist:
+ return Response({
+ 'code': 10,
+ 'msg': '处罚记录不存在或无权申诉',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 2. 验证处罚记录状态(必须是待处理状态0)
+ if chufa_record.sqzhuangtai != 0:
+ return Response({
+ 'code': 11,
+ 'msg': '该处罚记录已处理,无法申诉',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 验证订单是否存在(可选,根据需求决定)
+ # 这里可以根据dingdan_id去查询订单表,但根据需求描述,我们已经有了处罚记录
+ # 处罚记录中包含了dingdan_id,如果dingdan_id不存在,说明订单可能被删除
+ # 但根据业务逻辑,有处罚记录就应该有订单,所以这里可以跳过这个检查
+
+ # 4. 检查是否已有申诉记录(防止重复申诉)
+ if chufa_record.ssliyou or chufa_record.sqzhuangtai == 3:
+ # 检查是否已经有申诉图片
+ existing_shensu_tupian = Chufatupian.objects.filter(
+ dingdan_id=chufa_record.dingdan_id,
+ yonghuid=yonghuid
+ ).exists()
+
+ if existing_shensu_tupian:
+ return Response({
+ 'code': 12,
+ 'msg': '您已经提交过申诉,请等待处理结果',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 更新处罚记录
+ chufa_record.ssliyou = shensu_liyou
+ chufa_record.sqzhuangtai = 3 # 状态改为申诉中
+ chufa_record.save()
+
+ # 6. 保存申诉图片(如果有)
+ saved_tupian_urls = []
+ if shensu_tupian_urls:
+ for tupian_url in shensu_tupian_urls:
+ if tupian_url: # 确保URL不为空
+ chufa_tupian = Chufatupian.objects.create(
+ dingdan_id=chufa_record.dingdan_id,
+ yonghuid=yonghuid,
+ tupian=tupian_url
+ )
+ saved_tupian_urls.append(tupian_url)
+
+ # 7. 构建返回数据
+ response_data = {
+ 'chufa_id': chufa_record.id,
+ 'dingdan_id': chufa_record.dingdan_id or '',
+ 'shensu_liyou': shensu_liyou,
+ 'shensu_tupian_urls': saved_tupian_urls,
+ 'sqzhuangtai': chufa_record.sqzhuangtai,
+ 'update_time': chufa_record.update_time.strftime(
+ '%Y-%m-%d %H:%M:%S') if chufa_record.update_time else ''
+ }
+
+ # 记录操作日志
+ logger.info(f"打手申诉成功 - 用户ID: {yonghuid}, 处罚ID: {chufa_id}, 订单ID: {chufa_record.dingdan_id}")
+
+ return Response({
+ 'code': 0,
+ 'msg': '申诉提交成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ logger.error(f"打手申诉异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 99,
+ 'msg': '系统繁忙,请稍后重试',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
+
+class ShangjiaChufaJiluHuoquView(APIView):
+ """
+ 商家获取处罚记录接口
+ 路径: /yonghu/sjcfjlhq
+ 方法: POST
+ 权限: JWT Token + 商家权限验证
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 获取当前用户信息
+ user_main = request.user
+ yonghuid = user_main.yonghuid
+
+ # 通过反向关系获取商家扩展信息
+ try:
+ shangjia_profile = user_main.shop_profile
+ # 再次确认账号状态
+ if shangjia_profile.zhuangtai != 1:
+ return Response({
+ 'code': 3,
+ 'msg': '商家账号状态异常,无法查看处罚记录',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+ except UserShangjia.DoesNotExist:
+ return Response({
+ 'code': 2,
+ 'msg': '用户不是商家身份',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 判断请求类型
+ qingqiu_tongji = request.data.get('qingqiu_tongji', False)
+
+ if qingqiu_tongji:
+ return self.get_tongji_info(yonghuid)
+ else:
+ return self.get_chufa_list(yonghuid, request.data)
+
+ except Exception as e:
+ logger.error(f"商家获取处罚记录异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 99,
+ 'msg': '系统繁忙,请稍后重试',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def get_tongji_info(self, yonghuid):
+ """
+ 获取统计信息
+ """
+ try:
+ # 🔴 查询条件:qingqiuid = 当前商家ID
+ stats = Chufajilu.objects.filter(qingqiuid=yonghuid).aggregate(
+ zongshu=Count('id'),
+ daichuli=Count(
+ Case(
+ When(Q(sqzhuangtai=0) | Q(sqzhuangtai=3), then=1),
+ output_field=IntegerField()
+ )
+ ),
+ yichuli=Count(
+ Case(
+ When(Q(sqzhuangtai=1) | Q(sqzhuangtai=2), then=1),
+ output_field=IntegerField()
+ )
+ )
+ )
+
+ tongji_data = {
+ 'zongshu': stats.get('zongshu', 0) or 0,
+ 'daichuli': stats.get('daichuli', 0) or 0,
+ 'yichuli': stats.get('yichuli', 0) or 0
+ }
+
+ return Response({
+ 'code': 0,
+ 'msg': '获取统计信息成功',
+ 'data': tongji_data
+ })
+
+ except Exception as e:
+ logger.error(f"商家获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}")
+ return Response({
+ 'code': 1,
+ 'msg': '获取统计信息失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def get_chufa_list(self, yonghuid, request_data):
+ """
+ 获取处罚记录列表(商家端)
+ 🔴 只修改图片查询逻辑,字段保持原样
+ """
+ try:
+ # 获取分页参数
+ page = int(request_data.get('page', 1))
+ page_size = int(request_data.get('page_size', 10))
+
+ if page_size > 50:
+ page_size = 50
+ if page_size < 1:
+ page_size = 10
+
+ # 获取状态筛选参数
+ zhuangtai = request_data.get('zhuangtai', 0)
+
+ # 🔴 查询条件:qingqiuid = 当前商家ID
+ queryset = Chufajilu.objects.filter(qingqiuid=yonghuid)
+
+ if zhuangtai == 0:
+ queryset = queryset.filter(Q(sqzhuangtai=0) | Q(sqzhuangtai=3))
+ elif zhuangtai == 1:
+ queryset = queryset.filter(Q(sqzhuangtai=1) | Q(sqzhuangtai=2))
+ else:
+ pass
+
+ # 按创建时间倒序排列
+ queryset = queryset.order_by('-create_time')
+
+ # 分页逻辑
+ paginator = Paginator(queryset, page_size)
+
+ try:
+ page_obj = paginator.page(page)
+ except:
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': {
+ 'list': [],
+ 'has_more': False,
+ 'page': page,
+ 'page_size': page_size,
+ 'current_count': 0
+ }
+ })
+
+ chufa_records = list(page_obj)
+
+ if not chufa_records:
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': {
+ 'list': [],
+ 'has_more': False,
+ 'page': page,
+ 'page_size': page_size,
+ 'current_count': 0
+ }
+ })
+
+ # 🔴【核心修复】收集所有需要查询图片的组合
+ dingdan_ids = []
+ dashou_ids = []
+
+ for record in chufa_records:
+ if record.dingdan_id:
+ dingdan_ids.append(record.dingdan_id)
+ if record.dashouid: # 打手ID
+ dashou_ids.append(record.dashouid)
+
+ # 去重
+ dingdan_ids = list(set(dingdan_ids))
+ dashou_ids = list(set(dashou_ids))
+
+ # 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误)
+ # 1. 查询证据图片(商家自己上传的)
+ zhengju_mapping = {}
+ if dingdan_ids:
+ # 构建Q对象:dingdan_id在列表中 AND yonghuid=当前商家ID
+ zhengju_q = Q(dingdan_id__in=dingdan_ids, yonghuid=yonghuid)
+ zhengju_queryset = Chufatupian.objects.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian')
+
+ for item in zhengju_queryset:
+ key = f"{item['dingdan_id']}_{item['yonghuid']}"
+ if key not in zhengju_mapping:
+ zhengju_mapping[key] = []
+ if item['tupian']:
+ zhengju_mapping[key].append(item['tupian'])
+
+ # 2. 查询申诉图片(打手上传的)
+ shensu_mapping = {}
+ if dingdan_ids and dashou_ids:
+ # 构建Q对象:dingdan_id在列表中 AND yonghuid在打手ID列表中
+ shensu_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=dashou_ids)
+ shensu_queryset = Chufatupian.objects.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian')
+
+ for item in shensu_queryset:
+ key = f"{item['dingdan_id']}_{item['yonghuid']}"
+ if key not in shensu_mapping:
+ shensu_mapping[key] = []
+ if item['tupian']:
+ shensu_mapping[key].append(item['tupian'])
+
+ # 🔴【核心修复】组装返回数据 - 字段保持原样!
+ chufa_list = []
+ for record in chufa_records:
+ dingdan_id = record.dingdan_id
+ dashou_id = record.dashouid
+
+ # 获取证据图片(商家自己上传的)
+ zhengju_tupian = []
+ if dingdan_id:
+ key = f"{dingdan_id}_{yonghuid}"
+ zhengju_tupian = zhengju_mapping.get(key, [])
+
+ # 获取申诉图片(打手上传的)
+ shensu_tupian = []
+ if dingdan_id and dashou_id:
+ key = f"{dingdan_id}_{dashou_id}"
+ shensu_tupian = shensu_mapping.get(key, [])
+
+ # 🔴【字段保持原样!】与前端完全对应!
+ chufa_data = {
+ 'id': record.id,
+ 'dingdan_id': record.dingdan_id or '',
+ 'qingqiuid': record.dashouid or '', # 打手ID
+ 'cfliyou': record.cfliyou or '',
+ 'sqzhuangtai': record.sqzhuangtai or 0,
+ 'ssliyou': record.ssliyou or '',
+ 'jifen': record.jifen or 0,
+ 'create_time': record.create_time.strftime('%Y-%m-%d %H:%M:%S') if record.create_time else '',
+ 'update_time': record.update_time.strftime('%Y-%m-%d %H:%M:%S') if record.update_time else '',
+ 'zhengju_tupian': zhengju_tupian, # 商家自己上传的证据图片
+ 'shensu_tupian': shensu_tupian, # 打手上传的申诉图片
+ }
+ chufa_list.append(chufa_data)
+
+ # 判断是否还有更多数据
+ has_more = page_obj.has_next()
+
+ response_data = {
+ 'list': chufa_list,
+ 'has_more': has_more,
+ 'page': page,
+ 'page_size': page_size,
+ 'current_count': len(chufa_list)
+ }
+
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': response_data
+ })
+
+ except ValueError as e:
+ logger.error(f"商家端参数错误 - 用户ID: {yonghuid}, 错误: {str(e)}")
+ return Response({
+ 'code': 4,
+ 'msg': '参数格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+ except Exception as e:
+ logger.error(f"商家获取处罚记录列表异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 5,
+ 'msg': '获取记录列表失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+class KefuLoginView(APIView):
+ """
+ 客服登录接口(支持手机号重复)
+ 请求:POST /yonghu/kefujinru
+ 请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"}
+ 返回:
+ code=0 成功,data包含token,nicheng,yonghuid
+ code=4 账号被封禁(提示“账号已被封禁”)
+ 其他错误统一 code=1 msg="登录失败"
+ """
+ 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 or not erjimima:
+ return Response({
+ 'code': 1,
+ 'msg': '登录失败', # 统一提示,不暴露具体原因
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 获取客户端真实IP
+ client_ip = self.get_client_ip(request)
+
+ # 3. 查询所有手机号匹配且类型为客服的用户
+ # 注意:UserMain 中需要有 user_type 字段,且值为 'kefu' 代表客服
+ user_mains = UserMain.objects.filter(
+ phone=phone,
+ user_type='kefu' # 只查找客服账号
+ ).select_related('kefu_profile') # 预加载客服扩展表
+
+ if not user_mains.exists():
+ 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
+
+ for user in user_mains:
+ # 验证主密码(明文比较,按您的要求)
+ if user.password != password:
+ continue
+
+ # 检查是否存在客服扩展表
+ try:
+ kefu = user.kefu_profile
+ except UserKefu.DoesNotExist:
+ continue
+
+ # 验证二级密码(明文比较)
+ if kefu.erjimima != 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)
+ 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.last_login_time = timezone.now()
+ target_user.save(update_fields=['ip', 'last_login_time'])
+
+ # 6. 生成JWT token
+ refresh = RefreshToken.for_user(target_user)
+ token = str(refresh.access_token)
+
+ # 7. 返回成功数据
+ return Response({
+ 'code': 0,
+ 'msg': '登录成功',
+ 'data': {
+ 'token': token,
+ 'nicheng': target_kefu.nicheng,
+ 'yonghuid': target_user.yonghuid,
+ }
+ })
+
+ 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')
+
+
+
+
+
+
+logger = logging.getLogger(__name__)
+
+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.yonghuid}")
+ 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. 验证用户类型是否为客服
+ if current_user.user_type != 'kefu':
+ logger.warning(f"统计接口用户类型非客服: {current_user.yonghuid}")
+ return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 5. 尝试获取客服扩展表数据
+ try:
+ kefu_profile = current_user.kefu_profile # OneToOneField 反向关系
+ except UserKefu.DoesNotExist:
+ logger.warning(f"统计接口客服扩展表不存在: {current_user.yonghuid}")
+ 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
+ 参数:{"phone": "13800138000"}
+ 认证:JWT
+ 返回:code=0 + 类型列表
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ if not phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ current_user = request.user
+
+ # 2. 验证手机号一致性
+ if getattr(current_user, 'phone', '') != phone:
+ logger.warning(f"手机号不匹配: 请求phone={phone}, 用户phone={current_user.phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 验证用户类型及客服状态
+ if current_user.user_type != 'kefu':
+ logger.warning(f"用户类型非客服: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ kefu_profile = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu_profile.zhuangtai != 1:
+ logger.warning(f"客服账号已被禁用: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 4. 查询订单类型(商品类型)
+ try:
+ # 只查询需要的字段,减少数据传输
+ types_qs = ShangpinLeixing.objects.all().only('id', 'jieshao', 'tupian_url')
+ type_list = [
+ {
+ 'id': t.id,
+ 'biaoti': t.jieshao or '', # 假设商品类型的标题字段为 jieshao
+ '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': []})
+
+
+
+
+
+class KefuGetOrderListView(APIView):
+ """
+ 客服获取平台订单列表接口(发单平台=1,非跨平台)
+ 需要权限:002ab(平台订单管理)
+ 请求参数:
+ username: 客服账号(用于权限校验)
+ page: 页码
+ pageSize: 每页条数
+ dingdan_id: 订单ID(精确搜索)
+ laoban_id: 老板ID(搜索平台扩展表中的老板)
+ dashou_id: 打手ID(搜索主表中的接单打手)
+ leixing: 商品类型ID
+ zhuangtai: 状态(多个用逗号分隔,如 "1,7")
+ date: 日期(YYYY-MM-DD),筛选订单创建时间
+ nicheng: 游戏昵称(订单主表的 nicheng 字段)
+ jieshao: 订单介绍(订单主表的 jieshao 字段)
+ clkf: 处理客服(订单主表的 clkf 字段)
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ start_total = time.time()
+
+ # 1. 获取前端参数
+ username = request.data.get('phone', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ # 3. 检查平台订单管理权限(002ab)
+ if '002ab' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
+
+ # 4. 获取分页和筛选参数
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 20))
+
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ laoban_id = request.data.get('laoban_id', '').strip()
+ dashou_id = request.data.get('dashou_id', '').strip()
+ leixing = request.data.get('leixing')
+ zhuangtai = request.data.get('zhuangtai', '')
+ date = request.data.get('date', '').strip()
+ # 新增筛选条件
+ nicheng = request.data.get('nicheng', '').strip()
+ jieshao = request.data.get('jieshao', '').strip()
+ clkf = request.data.get('clkf', '').strip()
+
+ # ---------- 查询条件 ----------
+ # 基础条件:平台发单 + 非跨平台
+ base_q = Q(fadan_pingtai=1) & Q(is_cross=0)
+
+ if dingdan_id:
+ q_conditions = base_q & Q(dingdan_id=dingdan_id)
+ else:
+ q_conditions = base_q
+ if leixing is not None:
+ try:
+ leixing_int = int(leixing)
+ q_conditions &= Q(leixing_id=leixing_int)
+ except (ValueError, TypeError):
+ pass
+ if zhuangtai and zhuangtai != 'all':
+ try:
+ status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
+ if status_list:
+ q_conditions &= Q(zhuangtai__in=status_list)
+ except (ValueError, TypeError):
+ pass
+ if dashou_id:
+ q_conditions &= Q(jiedan_dashou_id=dashou_id)
+ if laoban_id:
+ q_conditions &= Q(pingtai_kuozhan__laoban_id=laoban_id)
+ if date:
+ q_conditions &= Q(create_time__date=date)
+ # 新增筛选条件
+ if nicheng:
+ q_conditions &= Q(nicheng__icontains=nicheng)
+ if jieshao:
+ q_conditions &= Q(jieshao__icontains=jieshao)
+ if clkf:
+ q_conditions &= Q(clkf__icontains=clkf)
+
+ # ---------- 统计(仅按发单平台,非跨平台) ----------
+ stats = Dingdan.objects.filter(fadan_pingtai=1, is_cross=0).aggregate(
+ total_orders=Count('id'),
+ completed_orders=Count('id', filter=Q(zhuangtai=3)),
+ refund_orders=Count('id', filter=Q(zhuangtai=5)),
+ pending_orders=Count('id', filter=Q(zhuangtai__in=[4,8]))
+ )
+
+ # ---------- 状态计数(所有状态都返回,非跨平台) ----------
+ status_counts = {
+ 'all': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0).count(),
+ '1,7': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai__in=[1,7]).count(),
+ '2': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=2).count(),
+ '8': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=8).count(),
+ '3': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=3).count(),
+ '4': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=4).count(),
+ '5': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=5).count(),
+ '6': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=6).count(),
+ }
+
+ # ---------- 分页查询 ----------
+ total_count = Dingdan.objects.filter(q_conditions).count()
+
+ orders_qs = Dingdan.objects.filter(q_conditions).select_related(
+ 'pingtai_kuozhan'
+ ).only(
+ 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', 'fadan_pingtai',
+ 'jine', 'tupian', 'create_time', 'nicheng', 'jiedan_dashou_id',
+ 'pingtai_kuozhan__laoban_id'
+ ).order_by('-create_time')[(page-1)*page_size : page*page_size]
+
+ # ---------- 获取老板昵称 ----------
+ laoban_ids = [o.pingtai_kuozhan.laoban_id for o in orders_qs if hasattr(o, 'pingtai_kuozhan') and o.pingtai_kuozhan.laoban_id]
+ nickname_map = {}
+ if laoban_ids:
+ users = UserMain.objects.filter(yonghuid__in=laoban_ids).select_related('boss_profile').only(
+ 'yonghuid', 'boss_profile__nickname'
+ )
+ for user in users:
+ nickname_map[user.yonghuid] = user.boss_profile.nickname or '' if hasattr(user, 'boss_profile') else ''
+
+ result_list = []
+ for order in orders_qs:
+ ext = getattr(order, 'pingtai_kuozhan', None)
+ laoban_id = ext.laoban_id if ext else ''
+ youxi_nicheng = nickname_map.get(laoban_id, order.nicheng or '')
+ result_list.append({
+ 'dingdan_id': order.dingdan_id or '',
+ 'jieshao': order.jieshao or '',
+ 'zhuangtai': order.zhuangtai,
+ 'leixing': order.leixing_id,
+ 'fadanpingtai': order.fadan_pingtai,
+ 'jiage': float(order.jine) if order.jine else 0.00,
+ 'tupian_url': order.tupian or '',
+ 'fadanshijian': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '',
+ 'youxi_nicheng': youxi_nicheng
+ })
+
+ response_data = {
+ 'stats': {
+ 'totalOrders': stats['total_orders'] or 0,
+ 'completedOrders': stats['completed_orders'] or 0,
+ 'refundOrders': stats['refund_orders'] or 0,
+ 'pendingOrders': stats['pending_orders'] or 0
+ },
+ 'statusCounts': status_counts,
+ 'list': result_list,
+ 'total': total_count,
+ 'page': page,
+ 'pageSize': page_size
+ }
+
+ logger.info(f"平台订单接口总耗时: {time.time()-start_total:.3f}s")
+ return Response({'code': 0, 'data': response_data})
+
+class KefuGetShangjiaOrderListView(APIView):
+ """
+ 客服获取商家订单列表接口(发单平台=2,非跨平台)
+ 需要权限:002ac(商家订单管理)—— 如不区分可使用 002ab
+ 请求参数:
+ username: 客服账号(用于权限校验)
+ page: 页码
+ pageSize: 每页条数
+ dingdan_id: 订单ID(精确搜索)
+ shangjia_id: 商家ID(搜索商家扩展表中的商家ID)
+ dashou_id: 打手ID(搜索主表中的接单打手)
+ leixing: 商品类型ID
+ zhuangtai: 状态(多个用逗号分隔,如 "1,7")
+ date: 日期(YYYY-MM-DD),筛选订单创建时间
+ nicheng: 游戏昵称(订单主表的 nicheng 字段)
+ jieshao: 订单介绍(订单主表的 jieshao 字段)
+ clkf: 处理客服(订单主表的 clkf 字段)
+ shangjia_nicheng: 商家昵称(商家扩展表中的昵称)
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ start_total = time.time()
+
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ # 3. 检查商家订单管理权限(使用 002ac,如果没有可改用 002ab)
+ if '002ac' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看商家订单列表'})
+
+ # 4. 获取分页和筛选参数
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 20))
+
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ shangjia_id = request.data.get('shangjia_id', '').strip()
+ dashou_id = request.data.get('dashou_id', '').strip()
+ leixing = request.data.get('leixing')
+ zhuangtai = request.data.get('zhuangtai', '')
+ date = request.data.get('date', '').strip()
+ # 新增筛选条件
+ nicheng = request.data.get('nicheng', '').strip()
+ jieshao = request.data.get('jieshao', '').strip()
+ clkf = request.data.get('clkf', '').strip()
+ shangjia_nicheng = request.data.get('shangjia_nicheng', '').strip()
+
+ # ---------- 查询条件 ----------
+ # 基础条件:商家发单 + 非跨平台
+ base_q = Q(fadan_pingtai=2) & Q(is_cross=0)
+
+ if dingdan_id:
+ q_conditions = base_q & Q(dingdan_id=dingdan_id)
+ else:
+ q_conditions = base_q
+ if leixing is not None:
+ try:
+ leixing_int = int(leixing)
+ q_conditions &= Q(leixing_id=leixing_int)
+ except (ValueError, TypeError):
+ pass
+ if zhuangtai and zhuangtai != 'all':
+ try:
+ status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
+ if status_list:
+ q_conditions &= Q(zhuangtai__in=status_list)
+ except (ValueError, TypeError):
+ pass
+ if dashou_id:
+ q_conditions &= Q(jiedan_dashou_id=dashou_id)
+ if shangjia_id:
+ q_conditions &= Q(shangjia_kuozhan__shangjia_id=shangjia_id)
+ if date:
+ q_conditions &= Q(create_time__date=date)
+ if nicheng:
+ q_conditions &= Q(nicheng__icontains=nicheng)
+ if jieshao:
+ q_conditions &= Q(jieshao__icontains=jieshao)
+ if clkf:
+ q_conditions &= Q(clkf__icontains=clkf)
+ if shangjia_nicheng:
+ # 通过商家扩展表关联 UserShangjia 的昵称
+ q_conditions &= Q(shangjia_kuozhan__shangjia_id__in=UserMain.objects.filter(
+ shop_profile__nicheng__icontains=shangjia_nicheng
+ ).values_list('yonghuid', flat=True))
+
+ # ---------- 统计(仅按发单平台,非跨平台) ----------
+ stats = Dingdan.objects.filter(fadan_pingtai=2, is_cross=0).aggregate(
+ total_orders=Count('id'),
+ completed_orders=Count('id', filter=Q(zhuangtai=3)),
+ refund_orders=Count('id', filter=Q(zhuangtai=5)),
+ pending_orders=Count('id', filter=Q(zhuangtai__in=[4,8]))
+ )
+
+ # ---------- 状态计数(所有状态都返回,非跨平台) ----------
+ status_counts = {
+ 'all': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0).count(),
+ '1,7': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai__in=[1,7]).count(),
+ '2': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=2).count(),
+ '8': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=8).count(),
+ '3': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=3).count(),
+ '4': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=4).count(),
+ '5': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=5).count(),
+ '6': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=6).count(),
+ }
+
+ # ---------- 分页查询 ----------
+ total_count = Dingdan.objects.filter(q_conditions).count()
+
+ orders_qs = Dingdan.objects.filter(q_conditions).select_related(
+ 'shangjia_kuozhan'
+ ).only(
+ 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', 'fadan_pingtai',
+ 'jine', 'tupian', 'create_time', 'nicheng', 'jiedan_dashou_id',
+ 'shangjia_kuozhan__shangjia_id'
+ ).order_by('-create_time')[(page-1)*page_size : page*page_size]
+
+ # ---------- 获取商家昵称 ----------
+ shangjia_ids = [o.shangjia_kuozhan.shangjia_id for o in orders_qs if hasattr(o, 'shangjia_kuozhan') and o.shangjia_kuozhan.shangjia_id]
+ nickname_map = {}
+ if shangjia_ids:
+ users = UserMain.objects.filter(yonghuid__in=shangjia_ids).select_related('shop_profile').only(
+ 'yonghuid', 'shop_profile__nicheng'
+ )
+ for user in users:
+ nickname_map[user.yonghuid] = user.shop_profile.nicheng or '' if hasattr(user, 'shop_profile') else ''
+
+ result_list = []
+ for order in orders_qs:
+ ext = getattr(order, 'shangjia_kuozhan', None)
+ shangjia_id = ext.shangjia_id if ext else ''
+ youxi_nicheng = order.nicheng or '' # 商家订单的玩家昵称
+ result_list.append({
+ 'dingdan_id': order.dingdan_id or '',
+ 'jieshao': order.jieshao or '',
+ 'zhuangtai': order.zhuangtai,
+ 'leixing': order.leixing_id,
+ 'fadanpingtai': order.fadan_pingtai,
+ 'jiage': float(order.jine) if order.jine else 0.00,
+ 'tupian_url': order.tupian or '',
+ 'fadanshijian': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '',
+ 'youxi_nicheng': youxi_nicheng
+ })
+
+ response_data = {
+ 'stats': {
+ 'totalOrders': stats['total_orders'] or 0,
+ 'completedOrders': stats['completed_orders'] or 0,
+ 'refundOrders': stats['refund_orders'] or 0,
+ 'pendingOrders': stats['pending_orders'] or 0
+ },
+ 'statusCounts': status_counts,
+ 'list': result_list,
+ 'total': total_count,
+ 'page': page,
+ 'pageSize': page_size
+ }
+
+ logger.info(f"商家订单接口总耗时: {time.time()-start_total:.3f}s")
+ return Response({'code': 0, 'data': response_data})
+
+
+
+
+
+
+
+
+
+
+
+class KefuChangeDashouView(APIView):
+ """
+ 客服更换打手接口
+ 请求:POST /yonghu/kefughds
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "new_dashou_id": "新打手ID"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ new_dashou_id = request.data.get('new_dashou_id', '').strip()
+
+ if not phone or not dingdan_id or not new_dashou_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.user_type != 'kefu':
+ logger.warning(f"用户类型非客服: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ kefu = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单
+ try:
+ order = Dingdan.objects.get(dingdan_id=dingdan_id)
+ except Dingdan.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 验证订单状态是否允许更换打手(进行中2 或 结算中8)
+ if order.zhuangtai not in [2, 8]:
+ return Response({'code': 400, 'msg': '订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 查询新打手
+ try:
+ new_dashou_user = UserMain.objects.get(yonghuid=new_dashou_id)
+ except UserMain.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 6. 验证打手扩展表
+ try:
+ new_dashou_profile = new_dashou_user.dashou_profile
+ except ObjectDoesNotExist:
+ return Response({'code': 404, 'msg': '打手信息不完整'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 7. 验证打手状态(打手状态和账号状态必须为1)
+ if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1:
+ return Response({'code': 400, 'msg': '打手状态不允许接单'}, status=status.HTTP_400_BAD_REQUEST)
+ # 8. 验证打手积分是否足够(至少5分)
+ if new_dashou_profile.jifen < 5:
+ return Response({'code': 400, 'msg': '打手积分不足,无法接单'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 8. 根据订单的抢单要求类型进行额外验证
+ yaoqiuleixing = order.yaoqiuleixing
+
+ if yaoqiuleixing == 1: # 会员抢单
+ huiyuan_id = order.huiyuan_id
+ if not huiyuan_id:
+ return Response({'code': 400, 'msg': '订单会员ID为空'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ # 查询打手的会员购买记录
+ huiyuan_goumai = Huiyuangoumai.objects.get(
+ huiyuan_id=huiyuan_id,
+ yonghu_id=new_dashou_id # 假设 yonghu_id 字段存储用户ID
+ )
+ except Huiyuangoumai.DoesNotExist:
+ return Response({'code': 400, 'msg': '打手未购买该订单要求的会员'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 检查会员是否过期(调用模型方法)
+ is_expired = huiyuan_goumai.jiance_shifou_daoqi()
+ if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1:
+ return Response({'code': 400, 'msg': '打手的会员已过期'}, status=status.HTTP_400_BAD_REQUEST)
+
+ elif yaoqiuleixing == 2: # 押金抢单
+ order_yajin = order.yongjin # 假设订单的 yongjin 字段存储押金要求
+ if order_yajin is None:
+ return Response({'code': 400, 'msg': '订单押金为空'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if new_dashou_profile.yajin < order_yajin:
+ return Response({'code': 400, 'msg': '打手押金不足'}, status=status.HTTP_400_BAD_REQUEST)
+ else:
+ return Response({'code': 400, 'msg': '订单抢单要求类型错误'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 9. 所有验证通过,开始更换打手(使用事务)
+ with transaction.atomic():
+ # 9.1 更新新打手扩展表
+ new_dashou_profile.zhuangtai = 0 # 设为接单中
+ new_dashou_profile.jiedanzongliang += 1
+ new_dashou_profile.jinrijiedan += 1
+ new_dashou_profile.jinyuejiedan += 1
+ new_dashou_profile.save()
+
+ # 9.2 处理原打手(如果存在)
+ old_dashou_id = order.jiedan_dashou_id
+ if old_dashou_id:
+ try:
+ old_dashou_user = UserMain.objects.get(yonghuid=old_dashou_id)
+ old_dashou_profile = old_dashou_user.dashou_profile
+ old_dashou_profile.zhuangtai = 1 # 恢复空闲
+ old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1(业务逻辑)
+ old_dashou_profile.save()
+ logger.info(f"原打手 {old_dashou_id} 状态已更新")
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"原打手 {old_dashou_id} 不存在或信息不完整,跳过更新")
+ except Exception as e:
+ logger.error(f"更新原打手时异常: {str(e)}", exc_info=True)
+
+ # 9.3 更新订单
+ order.jiedan_dashou_id = new_dashou_id
+ order.zhuangtai = 2 # 进行中
+ order.clkf = current_user.yonghuid # 记录处理客服ID
+ order.save()
+
+ return Response({'code': 0, 'msg': '更换打手成功'})
+
+
+
+
+
+class KefuRecoverOrderView(APIView):
+ """
+ 客服恢复订单接口(仅状态9可恢复)
+ 请求:POST /yonghu/kefuhf
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.user_type != 'kefu':
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ kefu = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单
+ try:
+ order = Dingdan.objects.get(dingdan_id=dingdan_id)
+ except Dingdan.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 验证订单状态是否为9(未付款)
+ if order.zhuangtai != 9:
+ return Response({'code': 400, 'msg': '订单状态不允许恢复'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 根据是否有指定ID决定新状态
+ if order.zhiding_id:
+ new_status = 7 # 指定中
+ else:
+ new_status = 1 # 下单中
+
+ # 6. 更新订单
+ order.zhuangtai = new_status
+ order.clkf = current_user.yonghuid
+ order.save()
+
+ return Response({'code': 0, 'msg': '恢复成功', 'data': {'new_status': new_status}})
+
+
+
+
+
+
+
+
+# dingdan/views.py
+class KefuPlatformRefundView(APIView):
+ """
+ 客服平台订单退款接口(调用微信支付)
+ 请求:POST /yonghu/kefupttk
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "tuikuan_liyou": "退款理由(可选)"
+ }
+ 认证:JWT
+ 返回:code=0 成功,code=其他 失败
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.user_type != 'kefu':
+ logger.warning(f"用户类型非客服: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ kefu = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单并加锁
+ try:
+ with transaction.atomic():
+ order = Dingdan.objects.select_related('pingtai_kuozhan').select_for_update().get(dingdan_id=dingdan_id)
+
+ # 4. 验证平台订单
+ if order.fadan_pingtai != 1:
+ return Response({'code': 400, 'msg': '该订单不是平台订单'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 验证状态允许退款(根据模型,状态值:1下单中,2进行中,3已完成,4退款审核,5已退款,6退款失败,7指定中,8结算中)
+ allowed_statuses = [1, 2, 4, 7, 8] # 允许退款的已支付或处理中状态
+ if order.zhuangtai not in allowed_statuses:
+ return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 6. 获取微信交易号(如果存在)
+ wechat_transaction_id = getattr(order, 'wechat_transaction_id', None)
+
+ # 7. 调用微信退款接口
+ refund_result = self.call_wechat_refund(
+ dingdan_id=order.dingdan_id,
+ jine=order.jine,
+ transaction_id=wechat_transaction_id
+ )
+
+ if refund_result['code'] != 0:
+ # 微信退款失败,直接返回错误,数据库不更新
+ logger.error(f"微信退款失败,订单 {dingdan_id},原因:{refund_result['msg']}")
+ return Response({'code': 400, 'msg': refund_result['msg']}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 8. 更新订单状态为5(已退款)
+ order.zhuangtai = 5
+ order.clkf = current_user.phone # 记录处理客服手机号
+ if tuikuan_liyou:
+ order.tkly = tuikuan_liyou
+ order.save()
+
+ # 9. 更新打手数据
+ jiedan_dashou_id = order.jiedan_dashou_id
+ if jiedan_dashou_id:
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ dashou_profile.tuikuanliang += 1
+ dashou_profile.zhuangtai = 1 # 打手状态设为空闲
+ dashou_profile.save()
+ logger.info(f"平台退款:打手 {jiedan_dashou_id} 退款订单+1")
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"平台退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
+
+ # ========== 新增:商品和店铺每日统计 ==========
+ try:
+ # 商品每日统计(只要有商品ID就执行)
+ if order.shangpin_id:
+ # 从扩展表获取已计算好的收益值(下单时已存储)
+ pingtai_shouyi = Decimal('0.00')
+ dianpu_shouyi = Decimal('0.00')
+ if hasattr(order, 'pingtai_kuozhan'):
+ pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00')
+ dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
+
+ update_shangpin_daily_stat(
+ shangpin_id=order.shangpin_id,
+ caozuo_leixing=3, # 3 = 退款
+ dingdan_jiage=order.jine,
+ pingtai_shouyi=pingtai_shouyi,
+ dianpu_zongshouyi=dianpu_shouyi
+ )
+ logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
+
+ # 店铺每日统计(仅当存在店铺ID时执行)
+ dianpu_id = None
+ if hasattr(order, 'pingtai_kuozhan'):
+ dianpu_id = order.pingtai_kuozhan.dianpu_id
+ if dianpu_id:
+ dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
+ update_dianpu_daily_stat(
+ dianpu_id=dianpu_id,
+ caozuo_leixing=3, # 3 = 退款
+ dingdan_jiage=order.jine,
+ dianpu_shouyi=dianpu_shouyi
+ )
+ logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
+ except Exception as e:
+ logger.error(f"商品/店铺统计更新失败: {e}")
+ logger.error(traceback.format_exc())
+ # 统计失败不影响主流程
+ # =============================================
+
+ # 10. 更新退款记录表(假设存在 Tuikuanjilu 模型,字段需根据实际调整)
+
+ #更新支出记录
+ update_daily_payout(order.jine)
+
+
+ try:
+ refund_record, created = Tuikuanjilu.objects.get_or_create(
+ dingdan_id=dingdan_id,
+ defaults={
+ 'dashouid': jiedan_dashou_id or '',
+ 'qingqiuid': '',
+ 'chuliid': phone,
+ 'liyou': tuikuan_liyou or '客服退款',
+ 'sqzhuangtai': 1, # 成功
+ 'jine': order.jine,
+ 'dashou_fencheng': order.dashou_fencheng or 0,
+ 'jieshao': order.jieshao or '',
+ 'beizhu': '客服平台退款',
+ 'nicheng': order.nicheng or '',
+ }
+ )
+ if not created:
+ refund_record.sqzhuangtai = 1
+ refund_record.chuliid = phone
+ if tuikuan_liyou:
+ refund_record.liyou = tuikuan_liyou
+ refund_record.save()
+ except Exception as e:
+ logger.error(f"平台退款更新退款记录异常: {str(e)}", exc_info=True)
+
+ # 11. 更新客服统计数据
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.jiedan_dashou_id,
+ amount=order.dashou_fencheng,
+ action=3 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+ logger.info(f"平台退款成功,订单 {dingdan_id},退款单号 {refund_result.get('refund_id')}")
+ return Response({'code': 0, 'msg': '退款成功'})
+
+ except Dingdan.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ logger.error(f"平台退款接口异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def call_wechat_refund(self, dingdan_id, jine, transaction_id=None):
+ """
+ 调用微信支付退款接口
+ :param dingdan_id: 商户订单号(我们的订单ID)
+ :param jine: 退款金额
+ :param transaction_id: 微信支付订单号(优先使用)
+ :return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'}
+ """
+ # 获取配置
+ appid = getattr(settings, 'WEIXIN_APPID', '')
+ mch_id = getattr(settings, 'WEIXIN_MCHID', '')
+ key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '')
+ cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '')
+ key_path = getattr(settings, 'WEIXIN_KEY_PATH', '')
+
+ if not all([appid, mch_id, key, cert_path, key_path]):
+ missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v]
+ logger.error(f"微信支付配置缺失: {missing}")
+ return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'}
+
+ # 生成退款单号
+ out_refund_no = self.generate_refund_no(dingdan_id)
+ total_fee = int(float(jine) * 100)
+ refund_fee = total_fee
+ refund_desc = '客服退款'
+
+ # 构造请求参数
+ params = {
+ 'appid': appid,
+ 'mch_id': mch_id,
+ 'nonce_str': self.generate_nonce_str(),
+ 'out_refund_no': out_refund_no,
+ 'total_fee': total_fee,
+ 'refund_fee': refund_fee,
+ 'refund_desc': refund_desc,
+ }
+ if transaction_id:
+ params['transaction_id'] = transaction_id
+ else:
+ params['out_trade_no'] = dingdan_id # 使用订单ID作为商户订单号
+
+ # 生成签名
+ sorted_keys = sorted(params.keys())
+ stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys])
+ stringSignTemp = f"{stringA}&key={key}"
+ sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
+ params['sign'] = sign
+
+ xml_data = self.dict_to_xml(params)
+ url = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
+
+ try:
+ response = requests.post(
+ url,
+ data=xml_data,
+ cert=(cert_path, key_path),
+ timeout=10
+ )
+ result = xmltodict.parse(response.content)['xml']
+ logger.info(f"微信退款响应: {result}")
+ except Exception as e:
+ logger.error(f"微信退款请求异常: {str(e)}", exc_info=True)
+ return {'code': 500, 'msg': '微信退款请求异常'}
+
+ if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
+ return {'code': 0, 'msg': 'success', 'refund_id': out_refund_no}
+ else:
+ err_msg = result.get('err_code_des', result.get('return_msg', '未知错误'))
+ logger.warning(f"微信退款失败: {err_msg}")
+ return {'code': 400, 'msg': err_msg}
+
+ def generate_nonce_str(self):
+ return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+
+ def generate_refund_no(self, dingdan_id):
+ timestamp = str(int(time.time()))
+ rand = ''.join(random.choices('0123456789', k=4))
+ return f"{dingdan_id}REF{timestamp}{rand}"
+
+ def dict_to_xml(self, data):
+ xml = ['']
+ for k, v in data.items():
+ xml.append(f'<{k}>{v}{k}>')
+ xml.append('')
+ return ''.join(xml)
+
+
+
+
+
+# dingdan/views.py
+class KefuRejectRefundView(APIView):
+ """
+ 客服拒绝退款接口
+ 请求:POST /yonghu/kefujjtk
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "jujue_liyou": "拒绝理由(可选)"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ jujue_liyou = request.data.get('jujue_liyou', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.user_type != 'kefu':
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ kefu = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单并加锁
+ try:
+ with transaction.atomic():
+ order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
+
+ # 4. 验证订单状态为退款审核中(4)
+ if order.zhuangtai != 4:
+ return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取必要字段
+ jiedan_dashou_id = order.jiedan_dashou_id
+ dashou_fencheng = order.dashou_fencheng
+ fadan_pingtai = order.fadan_pingtai
+
+ # 5. 验证打手分成金额是否存在
+ if dashou_fencheng is None:
+ return Response({'code': 400, 'msg': '订单缺少打手分成金额'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 6. 更新订单状态为退款失败(6)
+ order.zhuangtai = 6
+ order.clkf = current_user.phone # 记录处理客服手机号
+ if jujue_liyou:
+ order.tkly = jujue_liyou
+ order.save()
+
+ # 7. 给打手结算(返还分成)
+ if jiedan_dashou_id:
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ # 增加打手相关统计数据
+ dashou_profile.chengjiaozongliang += 1
+ dashou_profile.yue += dashou_fencheng
+ dashou_profile.zonge += dashou_fencheng
+ dashou_profile.jinrishouyi += dashou_fencheng
+ dashou_profile.jinyueshouyi += dashou_fencheng
+ dashou_profile.save()
+ logger.info(f"拒绝退款:打手 {jiedan_dashou_id} 获得分成 {dashou_fencheng}")
+ except (UserMain.DoesNotExist, AttributeError):
+ logger.warning(f"拒绝退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算")
+
+ # 8. 如果是商家发单,更新商家成交订单数量
+ if fadan_pingtai == 2:
+ try:
+ # 获取商家订单扩展表
+ shangjia_kuozhan = order.shangjia_kuozhan
+ shangjia_id = shangjia_kuozhan.shangjia_id
+ if shangjia_id:
+ shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
+ shangjia_profile = shangjia_user.shop_profile
+ shangjia_profile.chengjiao += 1
+ shangjia_profile.save()
+ logger.info(f"拒绝退款:商家 {shangjia_id} 成交订单+1")
+
+ # ✅ 新方法(行为驱动,更安全)
+ update_shangjia_daily(
+ yonghuid=shangjia_id,
+ amount=Decimal(str(order.jine)),
+ action=2 # 2 = 结算
+ )
+ except (ObjectDoesNotExist, UserMain.DoesNotExist, AttributeError):
+ logger.warning(f"拒绝退款:商家信息不存在,跳过商家更新")
+
+ # 9. 更新退款记录表
+ try:
+ refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id)
+ refund_record.sqzhuangtai = 2 # 2表示拒绝
+ refund_record.chuliid = phone
+ refund_record.bhliyou = jujue_liyou # 记录驳回理由(假设模型有此字段)
+ refund_record.save()
+ logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}")
+ except Tuikuanjilu.DoesNotExist:
+ # 如果退款记录不存在,可以创建一条记录(可选)
+ Tuikuanjilu.objects.create(
+ dingdan_id=dingdan_id,
+ dashouid=jiedan_dashou_id or '',
+ qingqiuid='',
+ chuliid=phone,
+ liyou=jujue_liyou or '客服拒绝退款',
+ sqzhuangtai=2,
+ jine=order.jine,
+ dashou_fencheng=dashou_fencheng or 0,
+ jieshao=order.jieshao or '',
+ beizhu='客服拒绝退款',
+ nicheng=order.nicheng or '',
+ bhliyou=jujue_liyou or '', # 假设模型有 bhliyou 字段
+ )
+ except Exception as e:
+ logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True)
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.jiedan_dashou_id,
+ amount=dashou_fencheng,
+ action=2 # 2 表示成交(结算)
+ )
+ logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+ # 10. 更新客服统计数据
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ return Response({'code': 0, 'msg': '拒绝退款成功'})
+
+ except Dingdan.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
+
+
+
+
+
+
+class KefuMerchantRefundView(APIView):
+ """
+ 客服商家订单退款接口(发单平台=2)
+ 请求:POST /yonghu/kefusjtk
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "tuikuan_liyou": "退款理由(可选)"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 客服身份验证(同前,省略以节省篇幅,但实际必须完整)
+ current_user = request.user
+ # ... 验证
+ if getattr(current_user, 'phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.user_type != 'kefu':
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ kefu = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+
+ # 查询订单,预加载商家扩展表
+ try:
+ order = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id)
+ except Dingdan.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 验证商家订单
+ if order.fadan_pingtai != 2:
+ return Response({'code': 400, 'msg': '该订单不是商家订单'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证状态允许退款
+ allowed_statuses = [1, 7, 2, 8, 4]
+ if order.zhuangtai not in allowed_statuses:
+ return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取必要数据
+ jiedan_dashou_id = order.jiedan_dashou_id
+ jine = order.jine
+ if jine is None:
+ return Response({'code': 400, 'msg': '订单金额缺失'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ shangjia_kuozhan = order.shangjia_kuozhan
+ shangjia_id = shangjia_kuozhan.shangjia_id
+ except ObjectDoesNotExist:
+ return Response({'code': 400, 'msg': '订单商家信息不存在'}, status=status.HTTP_400_BAD_REQUEST)
+
+ with transaction.atomic():
+ # 更新订单
+ order.zhuangtai = 5
+ order.clkf = current_user.phone
+ if tuikuan_liyou:
+ order.tkly = tuikuan_liyou
+ order.save()
+
+ # 更新打手
+ if jiedan_dashou_id:
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ dashou_profile.tuikuanliang += 1
+ dashou_profile.zhuangtai = 1
+ dashou_profile.save()
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"商家退款:打手 {jiedan_dashou_id} 不存在,跳过更新")
+
+ # 更新商家
+ try:
+ shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
+ shangjia_profile = shangjia_user.shop_profile
+ shangjia_profile.tuikuan += 1
+ shangjia_profile.yue += jine
+ shangjia_profile.save()
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"商家退款:商家 {shangjia_id} 不存在,跳过更新")
+
+ # 更新退款记录表
+ try:
+ refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id)
+ refund_record.sqzhuangtai = 1
+ refund_record.chuliid = phone
+ refund_record.save()
+ except Tuikuanjilu.DoesNotExist:
+ pass
+
+ # 更新客服统计
+ update_shangjia_daily(
+ yonghuid=shangjia_id,
+ amount=Decimal(str(order.jine)),
+ action=3 # 3 = 退款
+ )
+
+
+ # 调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.jiedan_dashou_id,
+ amount=order.dashou_fencheng,
+ action=3 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+
+
+ kefu = current_user.kefu_profile
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ return Response({'code': 0, 'msg': '退款成功'})
+
+
+
+class KefuRejectSettlementView(APIView):
+ """
+ 客服拒绝结算接口(仅平台订单,状态2/8)
+ 请求:POST /yonghu/kefujjjs
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "tkly": "拒绝结算理由(必填)"
+ }
+ 认证:JWT
+ 返回:code=0 成功,其他失败
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ tkly = request.data.get('tkly', '').strip()
+
+ if not phone or not dingdan_id or not tkly:
+ return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.user_type != 'kefu':
+ logger.warning(f"用户类型非客服: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ kefu = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单,预加载平台扩展表
+ try:
+ order = Dingdan.objects.select_related('pingtai_kuozhan').get(dingdan_id=dingdan_id)
+ except Dingdan.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 校验订单条件:平台发单(1)且状态为2或8
+ if order.fadan_pingtai != 1 or order.zhuangtai not in [2, 8]:
+ return Response({'code': 400, 'msg': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算'}, status=status.HTTP_400_BAD_REQUEST)
+
+ jiedan_dashou_id = order.jiedan_dashou_id
+ if not jiedan_dashou_id:
+ return Response({'code': 400, 'msg': '订单暂无接单打手'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 使用事务更新
+ with transaction.atomic():
+ # 5.1 更新订单主表
+ order.zhuangtai = 5 # 已退款
+ order.tkly = tkly # 拒绝理由
+ order.clkf = phone # 记录客服手机号
+ order.save()
+
+ # 5.2 更新平台订单扩展表:标记为拒绝结算(假设字段 jjjs_biaoshi)
+ # 注意:需要确保 DingdanPingtai 模型中已添加 jjjs_biaoshi 字段,IntegerField,默认0
+ if hasattr(order, 'pingtai_kuozhan') and order.pingtai_kuozhan:
+ pingtai_ext = order.pingtai_kuozhan
+ pingtai_ext.jjjs_biaoshi = 1 # 1表示拒绝结算
+ pingtai_ext.save()
+ else:
+ # 理论上应该存在,但若不存在则记录日志
+ logger.warning(f"订单 {dingdan_id} 缺少平台扩展表,无法标记拒绝结算")
+
+ # 5.3 更新打手扩展表
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ dashou_profile.zhuangtai = 1 # 打手状态设为空闲
+ dashou_profile.tuikuanliang += 1 # 退款订单总量+1
+ dashou_profile.save()
+ logger.info(f"拒绝结算:打手 {jiedan_dashou_id} 退款订单+1,状态空闲")
+ except (UserMain.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"拒绝结算:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
+ except Exception as e:
+ logger.error(f"拒绝结算更新打手异常: {str(e)}", exc_info=True)
+
+ # 5.4 更新客服扩展表统计数据
+
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.jiedan_dashou_id,
+ amount=order.dashou_fencheng,
+ action=3 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+
+
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ return Response({'code': 0, 'msg': '拒绝结算成功'})
+
+
+
+class KefuTransferHallView(APIView):
+ """
+ 客服恢复大厅(转移大厅)接口
+ 请求:POST /yonghu/kefuzydt
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证(同前,略,但必须完整实现)
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ if current_user.user_type != 'kefu':
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ try:
+ kefu = current_user.kefu_profile
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单
+ try:
+ order = Dingdan.objects.get(dingdan_id=dingdan_id)
+ except Dingdan.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 校验订单状态(允许状态:2,8,4)
+ if order.zhuangtai not in [2, 8, 4]:
+ return Response({'code': 400, 'msg': '订单状态不允许转移大厅'}, status=status.HTTP_400_BAD_REQUEST)
+
+
+ if order.jiedan_dashou_id:
+ UserDashou.objects.filter(user__yonghuid=order.jiedan_dashou_id).update(zhuangtai=1)
+ else:
+ pass
+
+ # 5. 根据是否有指定ID决定新状态
+ if order.zhiding_id:
+ new_status = 7 # 指定中
+ else:
+ new_status = 1 # 下单中
+
+ # 6. 使用事务更新
+ with transaction.atomic():
+ # 清空接单打手ID,更新状态,记录客服
+ order.jiedan_dashou_id = None
+ order.zhuangtai = new_status
+ order.clkf = phone
+ order.save()
+
+ return Response({'code': 0, 'msg': '转移大厅成功'})
+
+
+class KefuCancelDesignationView(APIView):
+ """
+ 客服取消指定接口(仅状态7)
+ 请求:POST /yonghu/kefuqxzd
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证(同前,略,但必须完整实现)
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ if current_user.user_type != 'kefu':
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ try:
+ kefu = current_user.kefu_profile
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单
+ try:
+ order = Dingdan.objects.get(dingdan_id=dingdan_id)
+ except Dingdan.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 校验订单状态为7(指定中)
+
+ if order.zhuangtai != 7:
+ print(999)
+ return Response({'code': 400, 'msg': '仅状态为指定中(7)的订单可取消指定'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 使用事务更新
+ with transaction.atomic():
+ # 状态改为1(下单中),清空指定ID,记录客服
+ order.zhuangtai = 1
+ order.zhiding_id = None
+ order.clkf = phone
+ order.save()
+
+ return Response({'code': 0, 'msg': '取消指定成功'})
+
+
+
+
+
+
+
+
+class KefuGetDashouListView(APIView):
+ """
+ 客服获取打手列表接口(仅打手类型)
+ 请求:POST /yonghu/kefuhqdslb
+ 参数:{
+ "phone": "客服手机号",
+ "keyword": "搜索关键词(可选,搜索用户ID或昵称)",
+ "page": "页码(从1开始)",
+ "page_size": "每页数量"
+ }
+ 认证:JWT
+ 返回:{
+ "code": 0,
+ "data": {
+ "list": [打手对象],
+ "has_more": true/false
+ }
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('zhanghao', '').strip()
+ keyword = request.data.get('keyword', '').strip()
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 20))
+ except ValueError:
+ return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if not phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.user_type != 'kefu':
+ logger.warning(f"用户类型非客服: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ kefu = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 构建查询条件(只查询打手)
+ # 通过 UserDashou 扩展表关联 UserMain
+ queryset = UserDashou.objects.select_related('user').all()
+
+ if keyword:
+ # 搜索用户ID或打手昵称
+ queryset = queryset.filter(
+ Q(user__yonghuid__icontains=keyword) |
+ Q(nicheng__icontains=keyword)
+ )
+
+ # 4. 计算总数并分页
+ total = queryset.count()
+ offset = (page - 1) * page_size
+ dashou_list = queryset.order_by('-create_time')[offset:offset + page_size]
+
+ # 5. 构建返回数据(只取需要的字段)
+ result = []
+ for dashou in dashou_list:
+ user = dashou.user
+ result.append({
+ 'yonghuid': user.yonghuid,
+ 'avatar': user.avatar or '',
+ 'zaixianzhuangtai': dashou.zaixianzhuangtai,
+ 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
+ 'nicheng': dashou.nicheng or '',
+ 'zhuangtai': dashou.zhuangtai,
+ })
+
+ # 6. 判断是否有更多数据
+ has_more = (offset + len(dashou_list)) < total
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': result,
+ 'has_more': has_more
+ }
+ })
+
+
+
+
+
+
+
+class KefuGetDashouDetailView(APIView):
+ """
+ 客服获取打手详情接口
+ 请求:POST /yonghu/kefuhqdsxq
+ 参数:{
+ "phone": "客服手机号",
+ "uid": "打手ID (yonghuid)"
+ }
+ 认证:JWT
+ 返回:code=0 + 打手信息 + 会员列表
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ uid = request.data.get('uid', '').strip()
+
+ if not phone or not uid:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.user_type != 'kefu':
+ logger.warning(f"用户类型非客服: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ kefu = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询打手主表和扩展表
+ try:
+ # 使用 select_related 预加载 dashou_profile,减少数据库查询
+ user_main = UserMain.objects.select_related('dashou_profile').get(
+ yonghuid=uid
+ )
+ except UserMain.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 验证用户类型是否为打手(可选,但建议验证)
+ # 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册)
+ try:
+ dashou = user_main.dashou_profile
+ except ObjectDoesNotExist:
+ # 用户存在但未注册打手身份,返回基本信息
+ user_info = {
+ 'yonghuid': user_main.yonghuid,
+ 'avatar': user_main.avatar or '',
+ 'zaixianzhuangtai': 0,
+ 'zhanghaozhuangtai': 0,
+ 'zhuangtai': 0,
+ 'nicheng': '',
+ 'chenghao': '',
+ 'dianhua': '',
+ 'wechat': '',
+ 'phone': user_main.phone or '',
+ 'create_time': user_main.create_time,
+ 'ip': user_main.ip or '',
+ 'jiedanzongliang': 0,
+ 'chengjiaozongliang': 0,
+ 'tuikuanliang': 0,
+ 'jinrijiedan': 0,
+ 'yue': 0.00,
+ 'zonge': 0.00,
+ 'jifen': 0,
+ 'yajin': 0.00,
+ 'jinrishouyi': 0.00,
+ 'jinyueshouyi': 0.00,
+ 'yaoqingren': '',
+ 'jieshao': '',
+ }
+ member_list = []
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'user_info': user_info,
+ 'member_list': member_list
+ }
+ })
+
+ # 4. 构建打手信息
+ user_info = {
+ 'yonghuid': user_main.yonghuid,
+ 'avatar': user_main.avatar or '',
+ 'zaixianzhuangtai': dashou.zaixianzhuangtai,
+ 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
+ 'zhuangtai': dashou.zhuangtai,
+ 'nicheng': dashou.nicheng or '',
+ 'chenghao': dashou.chenghao or '',
+ 'dianhua': dashou.dianhua or '',
+ 'wechat': dashou.wechat or '',
+ 'phone': user_main.phone or '',
+ 'create_time': user_main.create_time,
+ 'ip': user_main.ip or '',
+ 'jiedanzongliang': dashou.jiedanzongliang,
+ 'chengjiaozongliang': dashou.chengjiaozongliang,
+ 'tuikuanliang': dashou.tuikuanliang,
+ 'jinrijiedan': dashou.jinrijiedan,
+ 'yue': float(dashou.yue) if dashou.yue else 0.00,
+ 'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
+ 'jifen': dashou.jifen,
+ 'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
+ 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
+ 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
+ 'yaoqingren': dashou.yaoqingren or '',
+ 'jieshao': dashou.jieshao or '',
+ }
+
+ # 5. 查询打手会员信息(仅未过期)
+ member_records = Huiyuangoumai.objects.filter(
+ yonghu_id=uid
+ ).order_by('-create_time')
+
+ member_list = []
+ for record in member_records:
+ # 调用模型方法检查是否过期
+ if hasattr(record, 'jiance_shifou_daoqi'):
+ is_expired = record.jiance_shifou_daoqi()
+ else:
+ # 如果方法不存在,简单判断
+ is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
+
+ if not is_expired:
+ # 获取会员详情
+ try:
+ huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id)
+ huiyuan_name = huiyuan.jieshao
+ except Huiyuan.DoesNotExist:
+ huiyuan_name = '未知会员'
+
+ member_list.append({
+ 'id': record.id,
+ 'huiyuan_name': huiyuan_name,
+ 'daoqi_time': record.daoqi_time,
+ 'is_active': record.huiyuan_zhuangtai == 1,
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'user_info': user_info,
+ 'member_list': member_list
+ }
+ })
+
+
+
+
+class KefuUpdateDashouView(APIView):
+ permission_classes = [IsAuthenticated]
+ """
+ 客服修改打手信息接口
+ 支持操作:
+ - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb)
+ - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb)
+ - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd)
+ - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee)
+ - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee)
+ - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee)
+ - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg)
+ """
+ def post(self, request):
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ dashou_id = request.data.get('dashou_id', '').strip()
+ caozuo = request.data.get('caozuo', '').strip()
+ value = request.data.get('value') # 金额、积分、天数等
+ huiyuan_id = request.data.get('huiyuan_id', '').strip() # 会员ID(加/减会员时使用)
+
+ if not username or not dashou_id or not caozuo:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 权限校验(统一方法)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 查询打手是否存在
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id, user_type='dashou')
+ dashou_profile = dashou_user.dashou_profile
+ except UserMain.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手不存在'})
+ except UserDashou.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手扩展信息缺失'})
+
+ # 4. 根据操作类型执行(事务内)
+ with transaction.atomic():
+ try:
+ # 记录修改前的值(用于日志)
+ before = {
+ 'yue': dashou_profile.yue,
+ 'yajin': dashou_profile.yajin,
+ 'jifen': dashou_profile.jifen,
+ 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'zhuangtai': dashou_profile.zhuangtai,
+ }
+
+ # ---------- 余额操作 ----------
+ if caozuo == 'jian_yue':
+ if '001aa' not in permissions:
+ return Response({'code': 403, 'msg': '无权限减打手余额'})
+ amount = Decimal(str(value))
+ if dashou_profile.yue < amount:
+ return Response({'code': 400, 'msg': '余额不足'})
+ dashou_profile.yue -= amount
+ dashou_profile.save()
+ after_yue = dashou_profile.yue
+ # 记录日志
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2, # 2=打手
+ xiugaitijiao=after_yue,
+ xiugaitijiaoq=before['yue'],
+ )
+ return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after_yue)}})
+
+ elif caozuo == 'jia_yue':
+ if '001bb' not in permissions:
+ return Response({'code': 403, 'msg': '无权限加打手余额'})
+ amount = Decimal(str(value))
+ dashou_profile.yue += amount
+ dashou_profile.save()
+ after_yue = dashou_profile.yue
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ xiugaitijiao=after_yue,
+ xiugaitijiaoq=before['yue'],
+ )
+ return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after_yue)}})
+
+ # ---------- 押金操作 ----------
+ elif caozuo == 'jian_yajin':
+ if '001aa' not in permissions:
+ return Response({'code': 403, 'msg': '无权限减打手押金'})
+ amount = Decimal(str(value))
+ if dashou_profile.yajin < amount:
+ return Response({'code': 400, 'msg': '押金不足'})
+ dashou_profile.yajin -= amount
+ dashou_profile.save()
+ after_yajin = dashou_profile.yajin
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ yajin=after_yajin,
+ yyajin=before['yajin'],
+ )
+ return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after_yajin)}})
+
+ elif caozuo == 'jia_yajin':
+ if '001bb' not in permissions:
+ return Response({'code': 403, 'msg': '无权限加打手押金'})
+ amount = Decimal(str(value))
+ dashou_profile.yajin += amount
+ dashou_profile.save()
+ after_yajin = dashou_profile.yajin
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ yajin=after_yajin,
+ yyajin=before['yajin'],
+ )
+ return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after_yajin)}})
+
+ # ---------- 积分操作 ----------
+ elif caozuo == 'jian_jifen':
+ if '001cc' not in permissions:
+ return Response({'code': 403, 'msg': '无权限减打手积分'})
+ jifen = int(value)
+ if dashou_profile.jifen < jifen:
+ return Response({'code': 400, 'msg': '积分不足'})
+ dashou_profile.jifen -= jifen
+ dashou_profile.save()
+ after_jifen = dashou_profile.jifen
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ jifen=after_jifen,
+ yjifen=before['jifen'],
+ )
+ return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after_jifen}})
+
+ elif caozuo == 'jia_jifen':
+ if '001dd' not in permissions:
+ return Response({'code': 403, 'msg': '无权限加打手积分'})
+ jifen = int(value)
+ dashou_profile.jifen += jifen
+ dashou_profile.save()
+ after_jifen = dashou_profile.jifen
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ jifen=after_jifen,
+ yjifen=before['jifen'],
+ )
+ return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after_jifen}})
+
+ # ---------- 状态修改 ----------
+ elif caozuo == 'gai_zhanghaozhuangtai':
+ if '001ee' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改打手账号状态'})
+ new_status = int(value)
+ if new_status not in [0, 1]:
+ return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'})
+ dashou_profile.zhanghaozhuangtai = new_status
+ dashou_profile.save()
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ qitashuoming=f'修改账号状态为{new_status}',
+ )
+ return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}})
+
+ elif caozuo == 'gai_zaixianzhuangtai':
+ if '001ee' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改打手在线状态'})
+ new_status = int(value)
+ if new_status not in [0, 1]:
+ return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'})
+ dashou_profile.zaixianzhuangtai = new_status
+ dashou_profile.save()
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ qitashuoming=f'修改在线状态为{new_status}',
+ )
+ return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}})
+
+ elif caozuo == 'gai_zhuangtai':
+ if '001ee' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改打手接单状态'})
+ new_status = int(value)
+ if new_status not in [0, 1]:
+ return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'})
+ dashou_profile.zhuangtai = new_status
+ dashou_profile.save()
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ qitashuoming=f'修改接单状态为{new_status}',
+ )
+ return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}})
+
+ # ---------- 会员操作 ----------
+ elif caozuo == 'jia_huiyuan':
+ if '001ff' not in permissions:
+ return Response({'code': 403, 'msg': '无权限给打手加会员'})
+ if not huiyuan_id:
+ return Response({'code': 400, 'msg': '缺少会员ID'})
+ days = int(value) if value else 30 # 默认加30天
+ try:
+ huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id)
+ except Huiyuan.DoesNotExist:
+ return Response({'code': 404, 'msg': '会员类型不存在'})
+ # 获取或创建购买记录
+ record, created = Huiyuangoumai.objects.select_for_update().get_or_create(
+ yonghu_id=dashou_id,
+ huiyuan_id=huiyuan_id,
+ defaults={
+ 'huiyuan_zhuangtai': 1,
+ 'jieshao': huiyuan.jieshao,
+ 'daoqi_time': timezone.now() + timedelta(days=days),
+ }
+ )
+ if not created:
+ # 延长有效期
+ new_daoqi = record.daoqi_time + timedelta(days=days)
+ record.daoqi_time = new_daoqi
+ record.huiyuan_zhuangtai = 1
+ record.save()
+ # 记录修改日志
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ huiyuants=days,
+ huiyuan_id=huiyuan_id,
+ qitashuoming=f'加会员 {huiyuan_id},增加{days}天',
+ )
+ return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
+
+ elif caozuo == 'jian_huiyuan':
+ if '001gg' not in permissions:
+ return Response({'code': 403, 'msg': '无权限给打手减会员'})
+ if not huiyuan_id:
+ return Response({'code': 400, 'msg': '缺少会员ID'})
+ try:
+ record = Huiyuangoumai.objects.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id)
+ except Huiyuangoumai.DoesNotExist:
+ return Response({'code': 404, 'msg': '该打手未购买此会员'})
+ # 删除会员记录(或设置为过期,根据业务要求)
+ record.delete()
+ Xiugaijilu.objects.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.phone,
+ leixing=2,
+ huiyuan_id=huiyuan_id,
+ qitashuoming=f'移除会员 {huiyuan_id}',
+ )
+ return Response({'code': 0, 'msg': '减会员成功'})
+
+ else:
+ return Response({'code': 400, 'msg': '未知操作类型'})
+
+ except Exception as e:
+ logger.error(f"修改打手信息异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+
+
+
+
+
+class KefuPunishmentListView(APIView):
+ """
+ 客服获取处罚记录列表接口
+ 请求:POST /yonghu/kefu_cfgl
+ 参数:{
+ "phone": "客服手机号",
+ "status": [0,3] 或 [1,2], # 状态数组
+ "dingdan_id": "订单ID(可选)",
+ "dashouid": "打手ID(可选)",
+ "page": 1,
+ "page_size": 20
+ }
+ 认证:JWT
+ 返回:{
+ "code": 0,
+ "data": {
+ "list": [处罚记录],
+ "total": 总数,
+ "stats": {
+ "zongshu": 总记录数,
+ "daichuli": 待处理数,
+ "yichuli": 已处理数
+ }
+ }
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ status_list = request.data.get('status')
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ dashouid = request.data.get('dashouid', '').strip()
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 20))
+ except ValueError:
+ return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if not phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ # 1. 获取前端参数
+ username = request.data.get('phone', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ # 3. 检查平台订单管理权限(002ab)
+ if '005aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
+
+ # 3. 获取统计信息(不分页)
+ stats = {
+ 'zongshu': Chufajilu.objects.count(),
+ 'daichuli': Chufajilu.objects.filter(sqzhuangtai__in=[0, 3]).count(),
+ 'yichuli': Chufajilu.objects.filter(sqzhuangtai__in=[1, 2]).count(),
+ }
+
+ # 4. 构建查询条件
+ queryset = Chufajilu.objects.all()
+
+ if status_list and isinstance(status_list, list):
+ queryset = queryset.filter(sqzhuangtai__in=status_list)
+
+ if dingdan_id:
+ queryset = queryset.filter(dingdan_id=dingdan_id)
+
+ if dashouid:
+ queryset = queryset.filter(dashouid=dashouid)
+
+ # 5. 分页
+ total = queryset.count()
+ offset = (page - 1) * page_size
+ records = queryset.order_by('-create_time')[offset:offset + page_size]
+
+ # 6. 构建返回列表
+ list_data = []
+ for item in records:
+ list_data.append({
+ 'dingdan_id': item.dingdan_id,
+ 'dashouid': item.dashouid,
+ 'qingqiuid': item.qingqiuid,
+ 'cfliyou': item.cfliyou,
+ 'sqzhuangtai': item.sqzhuangtai,
+ 'create_time': item.create_time,
+ 'update_time': item.update_time,
+ # 其他字段按需添加
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': list_data,
+ 'total': total,
+ 'stats': stats
+ }
+ })
+
+
+
+
+
+
+class KefuGetOrderDetailView(APIView):
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 强制打印到控制台并立即刷新
+ def p(msg):
+ #print(f"[订单详情] {msg}")
+ sys.stdout.flush()
+
+ #p("========== 视图开始执行 ==========")
+
+ # 1. 打印原始请求数据
+ #p(f"request.data: {request.data}")
+
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ #p(f"phone={phone}, dingdan_id={dingdan_id}")
+
+ if not phone or not dingdan_id:
+ #p("参数不完整,返回401")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 获取当前用户
+ current_user = request.user
+
+
+ # 验证手机号
+ if getattr(current_user, 'phone', '') != phone:
+ #p("手机号不匹配")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 验证用户类型
+ if current_user.user_type != 'kefu':
+ #p("用户类型不是客服")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 客服扩展表
+ try:
+ kefu = current_user.kefu_profile
+ #p(f"kefu_profile: {kefu}")
+ if kefu.zhuangtai != 1:
+ #p(f"客服状态异常: {kefu.zhuangtai}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ except Exception as e:
+ #p(f"获取客服扩展表异常: {e}")
+ return Response({'code': 500, 'msg': '服务器错误'}, status=500)
+
+ # 3. 查询订单
+ try:
+ #p(f"开始查询订单: {dingdan_id}")
+ dingdan_obj = Dingdan.objects.select_related(
+ 'pingtai_kuozhan', 'shangjia_kuozhan'
+ ).get(dingdan_id=dingdan_id)
+ #p("订单查询成功")
+ except Dingdan.DoesNotExist:
+ #p("订单不存在")
+ return Response({'code': 404, 'msg': '订单不存在'}, status=404)
+ except Exception as e:
+ #p(f"订单查询异常: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '服务器错误'}, status=500)
+
+ # 4. 构建基础数据
+ try:
+ response_data = {
+ 'dingdan_id': dingdan_obj.dingdan_id or '',
+ 'zhuangtai': dingdan_obj.zhuangtai or 0,
+ 'fadanpingtai': dingdan_obj.fadan_pingtai or 0,
+ 'jiage': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00,
+ 'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00,
+ 'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '',
+ 'dashou_liuyan': dingdan_obj.dashou_liuyan or '',
+ 'zhiding_id': dingdan_obj.zhiding_id or '',
+ 'shangpin_id': dingdan_obj.shangpin_id or '',
+ 'leixing': dingdan_obj.leixing_id or 0,
+ 'tupian_url': dingdan_obj.tupian or '',
+ 'jieshao': dingdan_obj.jieshao or '',
+ 'beizhu': dingdan_obj.beizhu or '',
+ 'tkly': dingdan_obj.tkly or '',
+ 'nicheng': dingdan_obj.nicheng or '',
+ 'clkf': dingdan_obj.clkf or '',
+ 'chuangjianshijian': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.create_time else '',
+ 'genggaishijian': dingdan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.update_time else '',
+ }
+ #p("基础数据构建完成")
+ except Exception as e:
+ #p(f"构建基础数据异常: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '服务器错误'}, status=500)
+
+ # 5. 扩展表数据
+ try:
+ fadan_pingtai = dingdan_obj.fadan_pingtai
+ if fadan_pingtai == 1:
+ if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan:
+ pingtai = dingdan_obj.pingtai_kuozhan
+ response_data['pingtai_kuozhan'] = {
+ 'laoban_id': pingtai.laoban_id or '',
+ 'laoban_pingjia': pingtai.laoban_pingjia or '',
+ }
+ else:
+ response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''}
+ elif fadan_pingtai == 2:
+ if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan:
+ shangjia = dingdan_obj.shangjia_kuozhan
+ response_data['shangjia_kuozhan'] = {
+ 'shangjia_id': shangjia.shangjia_id or '',
+ 'sjnicheng': shangjia.sjnicheng or '',
+ 'shangjia_pingjia': shangjia.shangjia_pingjia or '',
+ 'cfliyou': shangjia.cfliyou or '',
+ 'sqzhuangtai': shangjia.sqzhuangtai if shangjia.sqzhuangtai is not None else 0,
+ 'bhliyou': shangjia.bhliyou or '',
+ }
+ else:
+ response_data['shangjia_kuozhan'] = {
+ 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '',
+ 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': ''
+ }
+ #p("扩展数据构建成功")
+ except Exception as e:
+ #p(f"扩展数据异常: {traceback.format_exc()}")
+ return Response({'code': 500, 'msg': '服务器错误'}, status=500)
+
+ # 6. 打手图片
+ try:
+ dashou_images = Dashoutupian.objects.filter(dingdan_id=dingdan_id).values_list('tupian', flat=True)
+ response_data['dashou_images'] = [img for img in dashou_images if img]
+ #p(f"打手图片数量: {len(response_data['dashou_images'])}")
+ except Exception as e:
+ #p(f"打手图片异常: {traceback.format_exc()}")
+ response_data['dashou_images'] = []
+
+ # 7. 退款理由
+ try:
+ tuikuan_record = Tuikuanjilu.objects.filter(dingdan_id=dingdan_id).order_by('-create_time').first()
+ response_data['tuikuan_liyou'] = tuikuan_record.liyou if tuikuan_record and tuikuan_record.liyou else ''
+ #p("退款理由查询完成")
+ except Exception as e:
+ #p(f"退款理由异常: {traceback.format_exc()}")
+ response_data['tuikuan_liyou'] = ''
+
+ #p("视图正常结束,返回成功")
+ return Response({'code': 0, 'data': response_data})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+class KefuPunishmentDetailView(APIView):
+ """
+ 客服获取处罚详情接口
+ 请求:POST /yonghu/kefu_cf_detail
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID"
+ }
+ 认证:JWT
+ 返回:code=0 + 处罚记录详情(含图片)
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.user_type != 'kefu':
+ logger.warning(f"用户类型非客服: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ kefu = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 查询处罚记录(按创建时间倒序取最新一条)
+ try:
+ record = Chufajilu.objects.filter(dingdan_id=dingdan_id).order_by('-create_time').first()
+ if not record:
+ return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ logger.error(f"查询处罚记录失败: {e}")
+ return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 查询商家证据图片
+ zhengju_images = []
+ shensu_images = []
+ try:
+ # 商家证据图片:上传者为请求人 (qingqiuid)
+ zhengju_qs = Chufatupian.objects.filter(dingdan_id=dingdan_id, yonghuid=record.qingqiuid)
+ zhengju_images = [item.tupian for item in zhengju_qs if item.tupian]
+
+ # 打手申诉图片:上传者为打手 (dashouid)
+ shensu_qs = Chufatupian.objects.filter(dingdan_id=dingdan_id, yonghuid=record.dashouid)
+ shensu_images = [item.tupian for item in shensu_qs if item.tupian]
+ except Exception as e:
+ logger.error(f"查询图片失败: {e}")
+
+ # 构建返回数据
+ data = {
+ 'dingdan_id': record.dingdan_id,
+ 'dashouid': record.dashouid,
+ 'qingqiuid': record.qingqiuid,
+ 'chuliid': record.chuliid,
+ 'cfliyou': record.cfliyou,
+ 'sqzhuangtai': record.sqzhuangtai,
+ 'bhliyou': record.bhliyou,
+ 'jifen': record.jifen,
+ 'ssliyou': record.ssliyou or '',
+ 'zhengju_tupian': zhengju_images,
+ 'shensu_tupian': shensu_images,
+ 'create_time': record.create_time,
+ 'update_time': record.update_time,
+ }
+
+ return Response({'code': 0, 'data': data})
+
+
+
+
+class KefuPunishmentActionView(APIView):
+ """
+ 客服同意/驳回处罚接口
+ 请求:POST /yonghu/kefu_cf_action
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "action": 1(同意) / 2(驳回),
+ "reject_reason": "驳回理由(驳回时必填)"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ action = request.data.get('action')
+ reject_reason = request.data.get('reject_reason', '').strip()
+
+ if not phone or not dingdan_id or action not in [1, 2]:
+ return Response({'code': 401, 'msg': '参数错误'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if action == 2 and not reject_reason:
+ return Response({'code': 400, 'msg': '驳回理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 客服身份验证(完整验证,此处略)
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ if current_user.user_type != 'kefu':
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ try:
+ kefu = current_user.kefu_profile
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 查询待处理的处罚记录(状态0或3)
+ try:
+ record = Chufajilu.objects.filter(
+ dingdan_id=dingdan_id,
+ sqzhuangtai__in=[0, 3]
+ ).order_by('-create_time').first()
+ if not record:
+ exists = Chufajilu.objects.filter(dingdan_id=dingdan_id).exists()
+ if exists:
+ return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST)
+ else:
+ return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ logger.error(f"查询处罚记录失败: {e}")
+ return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ with transaction.atomic():
+ # 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回
+ duplicate_records = Chufajilu.objects.filter(
+ dingdan_id=dingdan_id,
+ sqzhuangtai__in=[0, 3]
+ ).exclude(id=record.id)
+ for dup in duplicate_records:
+ dup.sqzhuangtai = 2
+ dup.chuliid = phone
+ dup.bhliyou = "系统自动驳回:存在重复处罚记录"
+ dup.save()
+ logger.info(f"自动驳回重复处罚记录 ID={dup.id}")
+
+ if action == 1: # 同意
+ # 可选验证打手存在
+ if record.dashouid:
+ try:
+ dashou = UserDashou.objects.get(user__yonghuid=record.dashouid)
+ except UserDashou.DoesNotExist:
+ return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST)
+
+ record.sqzhuangtai = 1
+ record.chuliid = phone
+ record.save()
+
+ # 更新商家订单扩展表
+ try:
+ dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
+ if hasattr(dingdan, 'shangjia_kuozhan'):
+ dingdan.shangjia_kuozhan.sqzhuangtai = 1
+ dingdan.shangjia_kuozhan.save()
+ except Exception as e:
+ logger.warning(f"更新商家扩展表失败: {e}")
+
+ elif action == 2: # 驳回
+ # 返还积分
+ if record.jifen > 0 and record.dashouid:
+ try:
+ dashou = UserDashou.objects.get(user__yonghuid=record.dashouid)
+ if dashou.jifen <= 1000: # 安全限制
+ dashou.jifen += record.jifen
+ dashou.save()
+ logger.info(f"驳回处罚,打手{record.dashouid}返还积分{record.jifen}")
+ else:
+ logger.warning(f"打手积分异常({dashou.jifen}),不再返还")
+ except UserDashou.DoesNotExist:
+ logger.warning(f"打手{record.dashouid}不存在,无法返还积分")
+
+ record.sqzhuangtai = 2
+ record.chuliid = phone
+ record.bhliyou = reject_reason
+ record.save()
+
+ try:
+ dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
+ if hasattr(dingdan, 'shangjia_kuozhan'):
+ dingdan.shangjia_kuozhan.sqzhuangtai = 2
+ dingdan.shangjia_kuozhan.bhliyou = reject_reason
+ dingdan.shangjia_kuozhan.save()
+ except Exception as e:
+ logger.warning(f"更新商家扩展表失败: {e}")
+
+ return Response({'code': 0, 'msg': '处理成功'})
+
+
+
+
+
+
+
+
+class KefuForceCompleteView(APIView):
+ """
+ 客服强制结单接口(仅本地订单)
+ 请求:POST /yonghu/kefuqiangzhijiedan
+ 参数:{"phone": "客服手机号", "dingdan_id": "订单ID"}
+ 认证:JWT,仅限状态正常的客服
+ 逻辑:
+ 1. 验证客服身份及状态
+ 2. 查询订单并加锁(事务内)
+ 3. 校验订单状态是否为8(结算中)
+ 4. 校验订单必须有接单打手且打手分成金额有效
+ 5. 给打手增加余额、成交统计、今日/本月收益
+ 6. 如果是商家发单,更新商家成交订单数量
+ 7. 更新订单状态为3(已完成)
+ 8. 更新客服处理统计
+ 9. 返回成功
+ """
+ permission_classes = [permissions.IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '参数缺失'})
+
+ # 1. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'})
+
+ if current_user.user_type != 'kefu':
+ return Response({'code': 401, 'msg': '认证失败'})
+
+ try:
+ kefu = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'})
+
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '客服状态异常'})
+
+ # 2. 订单处理(事务内)
+ try:
+ with transaction.atomic():
+ order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
+
+ # 3. 校验订单状态
+ if order.zhuangtai != 8:
+ return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'})
+
+ # 4. 校验必要字段
+ dashou_id = order.jiedan_dashou_id
+ dashou_fencheng = order.dashou_fencheng
+ if not dashou_id:
+ return Response({'code': 400, 'msg': '订单无接单打手'})
+ if not dashou_fencheng or dashou_fencheng <= 0:
+ return Response({'code': 400, 'msg': '打手分成金额无效'})
+
+ # 5. 给打手结算
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id)
+ dashou_profile = dashou_user.dashou_profile
+ # 原子更新(避免并发重复加)
+ dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1
+ dashou_profile.yue = F('yue') + dashou_fencheng
+ dashou_profile.zonge = F('zonge') + dashou_fencheng
+ dashou_profile.jinrishouyi = F('jinrishouyi') + dashou_fencheng
+ dashou_profile.jinyueshouyi = F('jinyueshouyi') + dashou_fencheng
+ dashou_profile.save(update_fields=[
+ 'chengjiaozongliang', 'yue', 'zonge',
+ 'jinrishouyi', 'jinyueshouyi'
+ ])
+ except (UserMain.DoesNotExist, AttributeError):
+ return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'})
+
+ # 6. 如果是商家发单,更新商家成交订单数量
+ if order.fadan_pingtai == 2:
+ try:
+ # 通过 DingdanShangjia 获取商家ID
+ shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan'
+ if shangjia_ext and shangjia_ext.shangjia_id:
+ shangjia_user = UserMain.objects.get(yonghuid=shangjia_ext.shangjia_id)
+ shangjia_profile = shangjia_user.shop_profile
+ shangjia_profile.chengjiao = F('chengjiao') + 1
+ shangjia_profile.save(update_fields=['chengjiao'])
+
+ # (行为驱动,更安全)
+ update_shangjia_daily(
+ yonghuid=shangjia_ext.shangjia_id,
+ amount=Decimal(str(order.jine)),
+ action=2 # 2 = 结算
+ )
+
+ except ObjectDoesNotExist:
+ # 没有商家扩展记录则忽略
+ pass
+
+ # 7. 更新订单状态
+ order.zhuangtai = 3
+ order.clkf = phone
+ order.save(update_fields=['zhuangtai', 'clkf'])
+
+
+ if order.fadan_pingtai == 1:
+
+ # ========== 新增:商品和店铺每日统计 ==========
+ try:
+ # 商品每日统计(只要有商品ID就执行)
+ if order.shangpin_id:
+ # 从扩展表获取已计算好的收益值(下单时已存储)
+ pingtai_shouyi = Decimal('0.00')
+ dianpu_shouyi = Decimal('0.00')
+ if hasattr(order, 'pingtai_kuozhan'):
+ pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00')
+ dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
+
+ update_shangpin_daily_stat(
+ shangpin_id=order.shangpin_id,
+ caozuo_leixing=2, # 2 = 结单
+ dingdan_jiage=order.jine,
+ pingtai_shouyi=pingtai_shouyi,
+ dianpu_zongshouyi=dianpu_shouyi
+ )
+ logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
+
+ # 店铺每日统计(仅当存在店铺ID时执行)
+ dianpu_id = None
+ if hasattr(order, 'pingtai_kuozhan'):
+ dianpu_id = order.pingtai_kuozhan.dianpu_id
+ if dianpu_id:
+ dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
+ update_dianpu_daily_stat(
+ dianpu_id=dianpu_id,
+ caozuo_leixing=2, # 2 = 结单
+ dingdan_jiage=order.jine,
+ dianpu_shouyi=dianpu_shouyi
+ )
+ logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
+ except Exception as e:
+ logger.error(f"商品/店铺统计更新失败: {e}")
+ logger.error(traceback.format_exc())
+ # 统计失败不影响主流程
+ # =============================================
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.jiedan_dashou_id,
+ amount=dashou_fencheng,
+ action=2 # 2 表示成交(结算)
+ )
+ logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+ # 商家订单管事分红
+ if order.fadan_pingtai == 2:
+ settle_shangjia_order_guanshi_fenhong(order, dashou_id)
+
+ # 8. 更新客服统计
+ kefu.jinrichuli = F('jinrichuli') + 1
+ kefu.jinyuechuli = F('jinyuechuli') + 1
+ kefu.zongchuli = F('zongchuli') + 1
+ kefu.save(update_fields=['jinrichuli', 'jinyuechuli', 'zongchuli'])
+
+ return Response({'code': 0, 'msg': '强制结单成功'})
+
+ except Dingdan.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'})
+ except Exception as e:
+ logger.error(f"强制结单异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+
+
+
+
+kefu_txsh_list_logger = logging.getLogger('yonghu.kefu_txsh_list')
+if not kefu_txsh_list_logger.handlers:
+ _console_handler = logging.StreamHandler(sys.stderr)
+ _console_handler.setFormatter(logging.Formatter(
+ '[%(asctime)s] %(levelname)s %(name)s: %(message)s'
+ ))
+ kefu_txsh_list_logger.addHandler(_console_handler)
+ kefu_txsh_list_logger.setLevel(logging.DEBUG)
+ kefu_txsh_list_logger.propagate = False
+
+_TIXIAN_LIST_BASE_FIELDS = (
+ 'id', 'yonghuid', 'avatar', 'phone', 'nicheng', 'leixing',
+ 'zhifu', 'skzhanghao', 'jine', 'zhuangtai', 'fangshi',
+ 'shenheid', 'bhliyou', 'create_time', 'update_time',
+)
+_TIXIAN_LIST_AUDIT_FIELDS = ('shenhe_jilu_id', 'shenhe_danhao')
+
+
+class KefuWithdrawListView(APIView):
+ """
+ 客服获取提现审核列表接口
+ 请求:POST /yonghu/kefu_txsh_list
+ 参数:{
+ "phone": "客服手机号",
+ "page": 1,
+ "page_size": 20,
+ "status": 1(待审核)/2(已处理),
+ "type": 1(打手佣金)/2(管事分红)/3(组长分红)/4(审核官分佣)/5(打手押金)/6(商家余额), 可选
+ "payment": 1(微信)/2(支付宝) 可选,
+ "search_uid": "用户ID搜索" 可选,
+ "date": "YYYY-MM-DD" 可选,
+ "min_amount": 0, # 最小提现金额(元)可选
+ "max_amount": 999999, # 最大提现金额(元)可选
+ "dakuan_mode": 1 # 打款方式:1手动打款 2自动打款,可选
+ }
+ 认证:JWT
+ 返回:{
+ "code": 0,
+ "data": {
+ "list": [提现记录],
+ "total": 总数,
+ "stats": {
+ "total": 总记录数,
+ "awaiting": 待审核数,
+ "processed": 已处理数,
+ "total_amount": "总金额(元)"
+ }
+ }
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def _safe_request_params(self, request):
+ try:
+ return dict(request.data)
+ except Exception:
+ return {'raw': str(request.data)}
+
+ def _shenhe_jilu_field_available(self):
+ """探测 shenhe_jilu_id 字段是否已在数据库中就绪"""
+ try:
+ Tixianjilu.objects.values_list('shenhe_jilu_id', flat=True).first()
+ return True
+ except DatabaseError as e:
+ kefu_txsh_list_logger.error(
+ 'kefu_txsh_list: shenhe_jilu_id 字段不可用,将按旧表结构查询: %s', e,
+ exc_info=True,
+ )
+ return False
+
+ def _list_queryset(self, q, shenhe_field_ok):
+ """字段未迁移时 only 旧字段,避免 SELECT 不存在的列导致 500"""
+ fields = _TIXIAN_LIST_BASE_FIELDS + (
+ _TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else ()
+ )
+ return Tixianjilu.objects.filter(q).only(*fields)
+
+ def _resolve_row_dakuan_mode(self, item, audit_mode_map, shenhe_field_ok):
+ """无关联审核或审核表未标记自动 → 默认手动(1)"""
+ if not shenhe_field_ok:
+ return 1
+ sid = getattr(item, 'shenhe_jilu_id', None)
+ if not sid:
+ return 1
+ if audit_mode_map.get(sid) == 2:
+ return 2
+ return 1
+
+ def post(self, request):
+ dakuan_mode_filter = request.data.get('dakuan_mode')
+ req_params = self._safe_request_params(request)
+ kefu_txsh_list_logger.info('kefu_txsh_list 请求开始 params=%s', req_params)
+
+ try:
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 20))
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if page < 1:
+ page = 1
+ if page_size < 1 or page_size > 100:
+ page_size = min(max(page_size, 1), 100)
+
+ status_filter = request.data.get('status')
+ type_filter = request.data.get('type')
+ payment_filter = request.data.get('payment')
+ search_uid = (request.data.get('search_uid') or '').strip()
+ date_filter = (request.data.get('date') or '').strip()
+ min_amount = request.data.get('min_amount')
+ max_amount = request.data.get('max_amount')
+
+ phone = (request.data.get('phone') or '').strip()
+ if not phone:
+ return Response({'code': 401, 'msg': '缺少客服手机号'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ kefu, permissions = verify_kefu_permission(request, phone)
+ if kefu is None:
+ return permissions
+ if '005bb' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
+
+ q = Q()
+
+ try:
+ status_val = int(status_filter) if status_filter is not None and str(status_filter).strip() != '' else None
+ except (TypeError, ValueError):
+ status_val = None
+
+ try:
+ dakuan_mode_val = int(dakuan_mode_filter) if dakuan_mode_filter is not None and str(dakuan_mode_filter).strip() != '' else None
+ except (TypeError, ValueError):
+ dakuan_mode_val = None
+
+ # 未传打款方式参数 → 默认手动打款(1)
+ effective_dakuan = dakuan_mode_val if dakuan_mode_val in (1, 2) else 1
+
+ if status_val == 1:
+ q &= Q(zhuangtai=1)
+ elif status_val == 2:
+ if effective_dakuan == 2:
+ q &= Q(zhuangtai__in=[2, 3, 4, 5, 6])
+ else:
+ q &= Q(zhuangtai__in=[2, 3])
+
+ if type_filter is not None and str(type_filter).strip() != '':
+ try:
+ type_val = int(type_filter)
+ if type_val in [1, 2, 3, 4, 5, 6]:
+ q &= Q(leixing=type_val)
+ except (TypeError, ValueError):
+ pass
+
+ if payment_filter is not None and str(payment_filter).strip() != '':
+ try:
+ payment_val = int(payment_filter)
+ if payment_val in [1, 2]:
+ q &= Q(fangshi=payment_val)
+ except (TypeError, ValueError):
+ pass
+
+ if search_uid:
+ q &= Q(yonghuid=search_uid)
+
+ if date_filter:
+ q &= Q(update_time__date=date_filter)
+
+ if min_amount is not None and str(min_amount).strip() != '':
+ try:
+ q &= Q(jine__gte=float(min_amount))
+ except (TypeError, ValueError):
+ pass
+ if max_amount is not None and str(max_amount).strip() != '':
+ try:
+ q &= Q(jine__lte=float(max_amount))
+ except (TypeError, ValueError):
+ pass
+
+ shenhe_field_ok = self._shenhe_jilu_field_available()
+ if shenhe_field_ok:
+ if effective_dakuan == 2:
+ q &= Q(shenhe_jilu_id__isnull=False)
+ else:
+ q &= Q(shenhe_jilu_id__isnull=True)
+ elif effective_dakuan == 2:
+ kefu_txsh_list_logger.warning('kefu_txsh_list: 自动打款筛选不可用,返回空列表')
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': [],
+ 'total': 0,
+ 'stats': {'total': 0, 'awaiting': 0, 'processed': 0, 'total_amount': '0.00'},
+ },
+ })
+
+ processed_statuses = [2, 3, 4, 5, 6] if effective_dakuan == 2 else [2, 3]
+
+ base_qs = self._list_queryset(q, shenhe_field_ok)
+ total_count = base_qs.count()
+ awaiting_count = self._list_queryset(q & Q(zhuangtai=1), shenhe_field_ok).count()
+ processed_count = self._list_queryset(
+ q & Q(zhuangtai__in=processed_statuses), shenhe_field_ok,
+ ).count()
+ agg = base_qs.aggregate(total=Sum('jine'))
+ total_amount = agg.get('total') or 0
+
+ stats = {
+ 'total': total_count,
+ 'awaiting': awaiting_count,
+ 'processed': processed_count,
+ 'total_amount': str(round(float(total_amount), 2)),
+ }
+
+ queryset = base_qs.order_by('create_time')
+ total = queryset.count()
+ records = list(queryset[(page - 1) * page_size: page * page_size])
+
+ audit_ids = [
+ getattr(item, 'shenhe_jilu_id', None)
+ for item in records if getattr(item, 'shenhe_jilu_id', None)
+ ]
+ audit_meta_map = {}
+ if audit_ids:
+ try:
+ audit_meta_map = load_audit_meta_map(audit_ids)
+ except Exception as e:
+ kefu_txsh_list_logger.error(
+ 'kefu_txsh_list: 查询审核表信息失败: %s', e,
+ exc_info=True,
+ )
+
+ audit_mode_map = {
+ aid: meta.get('dakuan_mode', 2) for aid, meta in audit_meta_map.items()
+ }
+ list_data = []
+ for item in records:
+ shenhe_jilu_id = getattr(item, 'shenhe_jilu_id', None) if shenhe_field_ok else None
+ shenhe_danhao = (getattr(item, 'shenhe_danhao', None) or '') if shenhe_field_ok else ''
+ if shenhe_jilu_id and not shenhe_danhao:
+ shenhe_danhao = audit_meta_map.get(shenhe_jilu_id, {}).get('shenhe_danhao', '')
+ list_data.append({
+ 'id': item.id,
+ 'shenhe_jilu_id': shenhe_jilu_id,
+ 'shenhe_danhao': shenhe_danhao,
+ 'yonghuid': item.yonghuid,
+ 'avatar': item.avatar or '',
+ 'phone': item.phone or '',
+ 'nicheng': item.nicheng or '',
+ 'leixing': item.leixing,
+ 'zhifu': item.zhifu or '',
+ 'skzhanghao': item.skzhanghao or '',
+ 'jine': str(item.jine) if item.jine is not None else '0.00',
+ 'zhuangtai': item.zhuangtai,
+ 'fangshi': item.fangshi,
+ 'dakuan_mode': self._resolve_row_dakuan_mode(item, audit_mode_map, shenhe_field_ok),
+ 'shenheid': item.shenheid or '',
+ 'bhliyou': item.bhliyou or '',
+ 'create_time': item.create_time.strftime('%Y-%m-%d %H:%M:%S') if item.create_time else '',
+ 'update_time': item.update_time.strftime('%Y-%m-%d %H:%M:%S') if item.update_time else '',
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': list_data,
+ 'total': total,
+ 'stats': stats,
+ },
+ })
+
+ except DatabaseError as e:
+ kefu_txsh_list_logger.error(
+ 'kefu_txsh_list 数据库错误 dakuan_mode=%s params=%s err=%s',
+ dakuan_mode_filter, req_params, e,
+ exc_info=True,
+ )
+ return Response({
+ 'code': 500,
+ 'msg': '数据库查询失败,请确认已执行 python manage.py migrate yonghu',
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+ except Exception as e:
+ kefu_txsh_list_logger.error(
+ 'kefu_txsh_list 接口异常 dakuan_mode=%s params=%s err=%s',
+ dakuan_mode_filter, req_params, e,
+ exc_info=True,
+ )
+ return Response({
+ 'code': 500,
+ 'msg': '获取提现列表失败',
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+class KefuWithdrawDetailView(APIView):
+ """
+ 客服获取提现详情接口
+ 请求:POST /yonghu/kefu_txsh_detail
+ 参数:{
+ "phone": "客服手机号",
+ "tixian_id": "提现记录ID"
+ }
+ 认证:JWT
+ 返回:code=0 + 提现记录详情 + 用户扩展信息
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ phone = request.data.get('phone', '').strip()
+ tixian_id = request.data.get('tixian_id')
+
+ if not phone or not tixian_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.user_type != 'kefu':
+ logger.warning(f"用户类型非客服: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ kefu = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 查询提现记录
+ try:
+ record = Tixianjilu.objects.get(id=tixian_id)
+ except Tixianjilu.DoesNotExist:
+ return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 查询用户主表
+ try:
+ user = UserMain.objects.get(yonghuid=record.yonghuid)
+ except UserMain.DoesNotExist:
+ user = None
+
+ # 构建基础返回数据(字段必须与前端一致)
+ data = {
+ 'id': record.id,
+ 'yonghuid': record.yonghuid,
+ 'avatar': record.avatar or '',
+ 'phone': record.phone or '',
+ 'nicheng': record.nicheng or '',
+ 'leixing': record.leixing, # 1打手 2管事
+ 'zhifu': record.zhifu or '',
+ 'skzhanghao': record.skzhanghao or '',
+ 'jine': str(record.jine) if record.jine else '0.00',
+ 'zhuangtai': record.zhuangtai,
+ 'fangshi': record.fangshi,
+ 'shenheid': record.shenheid or '',
+ 'bhliyou': record.bhliyou or '',
+ 'create_time': record.create_time,
+ 'update_time': record.update_time,
+ }
+
+ # 根据提现类型添加扩展信息
+ if user:
+ if record.leixing == 1: # 打手
+ try:
+ dashou = UserDashou.objects.get(user=user)
+ # 计算成交率和退款率
+ total = dashou.jiedanzongliang or 0
+ completed = dashou.chengjiaozongliang or 0
+ refund = dashou.tuikuanliang or 0
+ completion_rate = round((completed / total * 100) if total > 0 else 0)
+ refund_rate = round((refund / completed * 100) if completed > 0 else 0)
+
+ data['dashouInfo'] = {
+ 'zhuangtai': dashou.zhuangtai, # 工作状态
+ 'zaixianzhuangtai': dashou.zaixianzhuangtai, # 在线状态
+ 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, # 账号状态
+ 'yue': float(dashou.yue) if dashou.yue else 0.00, # 可提现余额
+ 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, # 接单总额
+ 'jifen': dashou.jifen, # 积分
+ 'jiedanzongliang': dashou.jiedanzongliang, # 接单总量
+ 'chengjiaozongliang': dashou.chengjiaozongliang, # 成交总量
+ 'tuikuanliang': dashou.tuikuanliang, # 退款总量
+ 'completionRate': completion_rate,
+ 'refundRate': refund_rate,
+ }
+ except ObjectDoesNotExist:
+ data['dashouInfo'] = None
+ elif record.leixing == 2: # 管事
+ try:
+ guanshi = UserGuanshi.objects.get(user=user)
+ data['guanshiInfo'] = {
+ 'zhuangtai': guanshi.zhuangtai, # 账号状态
+ 'yue': float(guanshi.yue) if guanshi.yue else 0.00,
+ 'yaogingshuliang': guanshi.yaogingshuliang, # 邀请打手总数
+ 'jinrichongzhi': guanshi.jinrichongzhi, # 今日充值
+ 'jinyuechongzhi': guanshi.jinyuechongzhi, # 今月充值
+ 'chongzhifenrun': float(guanshi.chongzhifenrun) if guanshi.chongzhifenrun else 0.00,
+ }
+ except ObjectDoesNotExist:
+ data['guanshiInfo'] = None
+
+ return Response({'code': 0, 'data': data})
+
+
+class KefuWithdrawActionView(APIView):
+ """
+ 客服处理提现接口(同意/拒绝)—— 仅此接口处理后台审核打款
+ POST /yonghu/kefu_txsh_action
+
+ 单条:{ phone, action, tixian_id, yonghuid, leixing, reason? }
+ 批量:{ phone, action, batch_list: [{tixian_id, yonghuid, leixing}, ...], reason? }
+
+ 自动打款(dakuan_mode=2):同意→6待收款;拒绝→5+驳回原因+退还可到账金额(shijidaozhang),手续费不退,不更新平台收支/每日统计
+ 手动打款(dakuan_mode=1):原逻辑不变
+ 状态要求:Tixianjilu.zhuangtai=1 且 TixianShenheJilu.zhuangtai=1(审核中),其他状态一律拒绝
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def _verify_kefu(self, request, phone):
+ if not phone:
+ return None, Response({'code': 401, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST)
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ if current_user.user_type != 'kefu':
+ return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ try:
+ kefu = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ if kefu.zhuangtai != 1:
+ return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ return current_user, None
+
+ def _parse_items(self, request):
+ """解析单条或 batch_list,返回 [{tixian_id, yonghuid, leixing}]"""
+ batch_list = request.data.get('batch_list')
+ if batch_list is not None:
+ if not isinstance(batch_list, list) or len(batch_list) == 0:
+ return None, Response({'code': 400, 'msg': 'batch_list 不能为空'}, status=status.HTTP_400_BAD_REQUEST)
+ items = []
+ for idx, row in enumerate(batch_list):
+ if not isinstance(row, dict):
+ return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 格式错误'}, status=status.HTTP_400_BAD_REQUEST)
+ tid = row.get('tixian_id')
+ yid = (row.get('yonghuid') or '').strip()
+ lx = row.get('leixing')
+ if not tid or not yid or lx is None:
+ return None, Response(
+ {'code': 400, 'msg': f'batch_list[{idx}] 缺少 tixian_id / yonghuid / leixing'},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+ try:
+ items.append({
+ 'tixian_id': int(tid),
+ 'yonghuid': yid,
+ 'leixing': int(lx),
+ })
+ except (TypeError, ValueError):
+ return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
+ return items, None
+
+ tixian_id = request.data.get('tixian_id')
+ if not tixian_id:
+ return None, Response({'code': 400, 'msg': '提现记录ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
+ yonghuid = (request.data.get('yonghuid') or '').strip()
+ leixing = request.data.get('leixing')
+ try:
+ item = {'tixian_id': int(tixian_id), 'yonghuid': yonghuid, 'leixing': int(leixing) if leixing is not None else None}
+ except (TypeError, ValueError):
+ return None, Response({'code': 400, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
+ return [item], None
+
+ def _process_auto_item(self, item, action, reason, kefu_user):
+ """自动打款单条处理(须在 transaction.atomic 内调用)"""
+ tixian_id = item['tixian_id']
+ req_yonghuid = item['yonghuid']
+ req_leixing = item['leixing']
+
+ if req_leixing not in [1, 2, 3, 4, 5, 6]:
+ raise ValueError(f'提现记录{tixian_id}:提现类型无效')
+
+ try:
+ tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
+ except Tixianjilu.DoesNotExist:
+ raise ValueError(f'提现记录{tixian_id}不存在')
+
+ if tixian.yonghuid != req_yonghuid:
+ raise ValueError(f'提现记录{tixian_id}:用户ID不匹配')
+ if tixian.leixing != req_leixing:
+ raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
+ if tixian.zhuangtai != 1:
+ raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
+ if not tixian.shenhe_jilu_id:
+ raise ValueError(f'提现记录{tixian_id}:非自动打款审核记录')
+
+ try:
+ audit = TixianShenheJilu.objects.select_for_update().get(id=tixian.shenhe_jilu_id)
+ except TixianShenheJilu.DoesNotExist:
+ raise ValueError(f'提现记录{tixian_id}:审核记录不存在')
+
+ if audit.yonghuid != req_yonghuid:
+ raise ValueError(f'提现记录{tixian_id}:审核记录用户ID不匹配')
+ if audit.leixing != req_leixing:
+ raise ValueError(f'提现记录{tixian_id}:审核记录提现类型不匹配')
+ if audit.zhuangtai != 1:
+ raise ValueError(f'提现记录{tixian_id}:审核记录非审核中状态')
+
+ try:
+ user = UserMain.objects.select_for_update().get(yonghuid=req_yonghuid)
+ except UserMain.DoesNotExist:
+ raise ValueError(f'用户{req_yonghuid}不存在')
+
+ if action == 1:
+ sync_audit_and_jilu_status(audit, 6, shenhe_ren_id=kefu_user.yonghuid)
+ tixian.shenheid = kefu_user.yonghuid
+ tixian.save(update_fields=['shenheid', 'update_time'])
+ else:
+ sync_audit_and_jilu_status(
+ audit, 5,
+ bhliyou=reason,
+ bo_hui_ren_id=kefu_user.yonghuid,
+ )
+ # 退还可到账金额,申请时扣的 shenqing_jine 中的手续费(shouxufei)不退
+ refund_balance(user, audit.leixing, audit.shijidaozhang)
+ tixian.shenheid = kefu_user.yonghuid
+ tixian.bhliyou = reason
+ tixian.save(update_fields=['shenheid', 'bhliyou', 'update_time'])
+
+ def _process_manual_item(self, item, action, reason, phone):
+ """手动打款单条处理(原逻辑,须在 transaction.atomic 内调用)"""
+ tixian_id = item['tixian_id']
+ try:
+ tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
+ except Tixianjilu.DoesNotExist:
+ raise ValueError(f'提现记录{tixian_id}不存在')
+
+ req_yonghuid = item.get('yonghuid')
+ req_leixing = item.get('leixing')
+ if req_yonghuid and tixian.yonghuid != req_yonghuid:
+ raise ValueError(f'提现记录{tixian_id}:用户ID不匹配')
+ if req_leixing is not None and tixian.leixing != req_leixing:
+ raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
+ if tixian.zhuangtai != 1:
+ raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
+
+ if getattr(tixian, 'shenhe_jilu_id', None):
+ raise ValueError(f'提现记录{tixian_id}:自动打款请走自动流程')
+
+ try:
+ user = UserMain.objects.get(yonghuid=tixian.yonghuid)
+ except UserMain.DoesNotExist:
+ raise ValueError(f'用户{tixian.yonghuid}不存在')
+
+ if action == 1:
+ tixian.zhuangtai = 2
+ try:
+ update_daily_payout(tixian.jine)
+ szjilu, _ = Szjilu.objects.get_or_create(id=1)
+ szjilu.zongsy -= tixian.jine
+ szjilu.zongzc += tixian.jine
+ szjilu.jrzc += tixian.jine
+ szjilu.update_time = timezone.now()
+ szjilu.save()
+ except Exception as e:
+ logger.error(f'更新收支记录失败: {e}')
+ else:
+ try:
+ refund_balance(user, tixian.leixing, tixian.jine)
+ except ValueError as e:
+ raise ValueError(str(e))
+ tixian.zhuangtai = 3
+ tixian.bhliyou = reason
+
+ tixian.shenheid = phone
+ tixian.update_time = timezone.now()
+ tixian.save()
+
+ def post(self, request):
+ logger.info(f'接收到提现处理请求,数据: {request.data}')
+
+ phone = request.data.get('phone', '').strip()
+ action_raw = request.data.get('action')
+ reason = request.data.get('reason', '').strip()
+
+ try:
+ action = int(action_raw)
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': 'action 参数必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
+ if action not in [1, 2]:
+ return Response({'code': 400, 'msg': 'action 必须为1(同意)或2(拒绝)'}, status=status.HTTP_400_BAD_REQUEST)
+ if action == 2 and not reason:
+ return Response({'code': 400, 'msg': '拒绝提现时理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
+
+ kefu_user, err_resp = self._verify_kefu(request, phone)
+ if err_resp:
+ return err_resp
+
+ items, err_resp = self._parse_items(request)
+ if err_resp:
+ return err_resp
+
+ is_batch = isinstance(request.data.get('batch_list'), list) and len(request.data.get('batch_list')) > 0
+
+ try:
+ with transaction.atomic():
+ for item in items:
+ tixian_id = item['tixian_id']
+ try:
+ tixian_probe = Tixianjilu.objects.select_for_update().get(id=tixian_id)
+ except Tixianjilu.DoesNotExist:
+ raise ValueError(f'提现记录{tixian_id}不存在')
+
+ # 有关联审核记录(shenhe_jilu_id)即为 zddksh 自动打款,不依赖 dakuan_mode 字段
+ is_auto = bool(getattr(tixian_probe, 'shenhe_jilu_id', None))
+
+ if is_auto:
+ self._process_auto_item(item, action, reason, kefu_user)
+ else:
+ if is_batch:
+ raise ValueError(f'提现记录{tixian_id}:批量操作仅支持自动打款记录')
+ self._process_manual_item(item, action, reason, phone)
+
+ except ValueError as e:
+ return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST)
+ except Exception as e:
+ logger.error(f'处理提现事务失败: {e}', exc_info=True)
+ return Response({'code': 500, 'msg': f'处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ msg = '批量处理成功' if is_batch and len(items) > 1 else '处理成功'
+ return Response({'code': 0, 'msg': msg, 'data': {'count': len(items)}})
+
+
+
+
+
+
+
+
+# dingdan/views.py
+class KefuPunishView(APIView):
+ """
+ 客服处罚打手接口
+ 请求:POST /yonghu/kefuchufa
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "reason": "处罚原因(可选)"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ reason = request.data.get('reason', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.user_type != 'kefu':
+ logger.warning(f"用户类型非客服: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ kefu = current_user.kefu_profile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单
+ try:
+ order = Dingdan.objects.get(dingdan_id=dingdan_id)
+ except Dingdan.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 获取打手ID
+ dashou_id = order.jiedan_dashou_id
+ if not dashou_id:
+ return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 查询打手
+ try:
+ dashou_user = UserMain.objects.get(yonghuid=dashou_id)
+ dashou = dashou_user.dashou_profile
+ except UserMain.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND)
+ except ObjectDoesNotExist:
+ return Response({'code': 400, 'msg': '该用户不是打手'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 6. 使用事务
+ with transaction.atomic():
+ # 创建处罚记录(积分扣除5分)
+ jifen = 5
+ chufa = Chufajilu.objects.create(
+ dashouid=dashou_id,
+ qingqiuid=phone, # 客服手机号作为请求人
+ cfliyou=reason or '',
+ sqzhuangtai=0, # 待处理
+ jifen=jifen,
+ dingdan_id=dingdan_id,
+ )
+
+ # 扣除打手积分
+ if dashou.jifen >= jifen:
+ dashou.jifen -= jifen
+ else:
+ dashou.jifen = 0
+ dashou.save()
+
+ # 可选:更新订单的 clkf 字段记录处理客服
+ order.clkf = phone
+ order.save()
+
+ return Response({'code': 0, 'msg': '处罚成功'})
+
+
+
+class AdKftjView(APIView):
+ """
+ 添加客服接口
+ 权限:管理员JWT
+ 请求:POST /yonghu/adkftj
+ 参数:{
+ "zhanghao": "管理员账号",
+ "phone": "客服手机号(必填)",
+ "password": "密码(必填)",
+ "erjimima": "二级密码(必填)",
+ "nicheng": "昵称(可选)"
+ }
+ 返回:code=0
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 参数
+ zhanghao = request.data.get('zhanghao', '').strip()
+ phone = request.data.get('phone', '').strip()
+ password = request.data.get('password', '').strip()
+ erjimima = request.data.get('erjimima', '').strip()
+ nicheng = request.data.get('nicheng', '').strip()
+
+ if not all([zhanghao, phone, password, erjimima]):
+ return Response({'code': 400, 'message': '手机号、密码、二级密码不能为空'})
+
+ # 2. 管理员验证(同前)
+ current_user = request.user
+ if current_user.phone != zhanghao or current_user.user_type != 'admin':
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ try:
+ admin_profile = current_user.admin_profile
+ except:
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 检查手机号是否已被使用
+ if UserMain.objects.filter(phone=phone).exists():
+ return Response({'code': 400, 'message': '该手机号已被注册'})
+
+ # 4. 生成唯一 yonghuid
+ def generate_yonghuid():
+ for _ in range(10):
+ timestamp = str(int(time.time()))[-5:]
+ rand = str(random.randint(0, 99)).zfill(2)
+ uid = timestamp + rand
+ if len(uid) == 7 and not UserMain.objects.filter(yonghuid=uid).exists():
+ return uid
+ raise Exception('无法生成唯一用户ID')
+
+ with transaction.atomic():
+ # 创建主表用户
+ user = UserMain.objects.create(
+ yonghuid=generate_yonghuid(),
+ phone=phone,
+ password=password,
+ user_type='kefu',
+ openid=f'kefu_{phone}' # 临时openid
+ )
+ # 创建客服扩展表
+ kefu = UserKefu.objects.create(
+ user=user,
+ nicheng=nicheng or f'客服{user.yonghuid}',
+ erjimima=erjimima, # 建议实际项目中加密存储
+ zhuangtai=1,
+ jinrichuli=0,
+ jinyuechuli=0,
+ zongchuli=0
+ )
+
+ return Response({'code': 0, 'message': '添加成功'})
+
+
+
+class AdKfglView(APIView):
+ """
+ 客服管理列表接口
+ 权限:管理员JWT
+ 请求:POST /yonghu/adkfgl
+ 参数:{
+ "zhanghao": "管理员账号",
+ "keyword": "搜索关键词(可选,匹配客服ID或昵称)"
+ }
+ 返回:{
+ "code": 0,
+ "data": {
+ "list": [客服对象...]
+ }
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 参数获取
+ zhanghao = request.data.get('zhanghao', '').strip()
+ keyword = request.data.get('keyword', '').strip()
+
+ if not zhanghao:
+ return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 管理员身份验证
+ current_user = request.user
+ if not hasattr(current_user, 'phone') or str(current_user.phone) != zhanghao:
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.user_type != 'admin':
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ admin_profile = current_user.admin_profile
+ except AttributeError:
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询所有客服(通过扩展表关联主表)
+ queryset = UserKefu.objects.select_related('user').all()
+
+ if keyword:
+ queryset = queryset.filter(
+ #Q(user__yonghuid__icontains=keyword) |
+ Q(nicheng__icontains=keyword) |
+ Q(user__phone__icontains=keyword)
+ )
+
+ # 4. 构建返回数据
+ kefu_list = []
+ for kefu in queryset:
+ user = kefu.user
+ kefu_list.append({
+ 'yonghuid': user.phone,
+ 'phone': user.phone or '',
+ 'nicheng': kefu.nicheng or '',
+ 'zhuangtai': kefu.zhuangtai,
+ 'jinrichuli': kefu.jinrichuli,
+ 'jinyuechuli': kefu.jinyuechuli,
+ 'zongchuli': kefu.zongchuli,
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {'list': kefu_list}
+ })
+
+
+
+
+
+class AdKfxgView(APIView):
+ """
+ 修改客服信息接口
+ 权限:管理员JWT
+ 请求:POST /yonghu/adkfxg
+ 参数:{
+ "zhanghao": "管理员账号",
+ "uid": "客服ID (yonghuid)",
+ "nicheng": "新昵称(可选)",
+ "phone": "新手机号(可选)",
+ "password": "新密码(可选)",
+ "erjimima": "新二级密码(可选)",
+ "zhuangtai": 0/1(可选)
+ }
+ 返回:code=0
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ zhanghao = request.data.get('zhanghao', '').strip()
+ uid = request.data.get('uid', '').strip()
+
+ if not zhanghao or not uid:
+ return Response({'code': 400, 'message': '参数不完整'})
+
+ # 管理员验证
+ current_user = request.user
+ if current_user.phone != zhanghao or current_user.user_type != 'admin':
+ return Response({'code': 401, 'message': '身份验证失败'})
+ try:
+ admin_profile = current_user.admin_profile
+ except:
+ return Response({'code': 401, 'message': '身份验证失败'})
+
+ # 查询客服
+ try:
+ user = UserMain.objects.get(phone=uid)
+ kefu = user.kefu_profile
+ except (UserMain.DoesNotExist, UserKefu.DoesNotExist):
+ return Response({'code': 404, 'message': '客服不存在'})
+
+ # 更新数据
+ with transaction.atomic():
+ # 更新主表字段
+ if 'phone' in request.data:
+ new_phone = request.data.get('phone').strip()
+ if new_phone and UserMain.objects.filter(phone=new_phone).exclude(phone=uid).exists():
+ return Response({'code': 400, 'message': '手机号已被使用'})
+ user.phone = new_phone
+ if 'password' in request.data:
+ user.password = request.data['password']
+ user.save()
+
+ # 更新扩展表
+ if 'nicheng' in request.data:
+ kefu.nicheng = request.data['nicheng']
+ if 'erjimima' in request.data:
+ kefu.erjimima = request.data['erjimima']
+ if 'zhuangtai' in request.data:
+ kefu.zhuangtai = int(request.data['zhuangtai'])
+ kefu.save()
+
+ return Response({'code': 0, 'message': '修改成功'})
+
+
+
+
+
+
+
+
+class WechatLoginAndGuanshiRegisterView(APIView):
+ """
+ 未登录用户微信登录+管事注册一体化接口
+ 路径: /api/yonghu/zuzhangyqmzc
+ 方法: POST
+ 权限: 允许所有
+ 请求体: { "code": "微信登录code", "inviteCode": "组长邀请码" }
+ 成功返回 code: 200,数据同 GuanshiRegisterView
+ """
+ permission_classes = [AllowAny]
+
+ def post(self, request):
+ code = request.data.get('code', '').strip()
+ yaoqingma = request.data.get('inviteCode', '').strip()
+
+ if not code:
+ return Response({'code': 400, 'msg': '微信授权码不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if not yaoqingma:
+ return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if len(yaoqingma) > 100:
+ return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ wechat_data = self._get_wechat_openid(code)
+ if not wechat_data or 'openid' not in wechat_data:
+ error_msg = wechat_data.get('errmsg', '微信授权失败')
+ return Response({'code': 400, 'msg': f'微信登录失败: {error_msg}', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ openid = wechat_data['openid']
+ kehuduan_ip = self._huoqu_kehuduan_ip(request)
+
+ with transaction.atomic():
+ user_main, created = UserMain.objects.select_for_update().get_or_create(
+ openid=openid,
+ defaults={
+ 'yonghuid': self._shengcheng_yonghu_id(),
+ 'user_type': 'normal',
+ }
+ )
+ user_main.ip = kehuduan_ip
+ user_main.last_login_time = timezone.now()
+ user_main.save()
+
+ if created:
+ UserBoss.objects.create(user=user_main, nickname='微信用户')
+
+ if hasattr(user_main, 'guanshi_profile'):
+ return self._fanhui_xianyou_guanshi_info(user_main)
+
+ try:
+ zuzhang = UserZuzhang.objects.select_related('user').get(yaoqingma=yaoqingma)
+ except UserZuzhang.DoesNotExist:
+ return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND)
+
+ if zuzhang.zhuangtai != 1:
+ return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None},
+ status=status.HTTP_403_FORBIDDEN)
+
+ zuzhang_yonghuid = zuzhang.user.yonghuid
+
+ guanshi_yaoqingma = CreateInvitationCode(str(user_main.yonghuid))
+ guanshi_profile = UserGuanshi.objects.create(
+ user=user_main,
+ yaoqingma=guanshi_yaoqingma,
+ yaoqingren=zuzhang_yonghuid,
+ )
+
+ UserZuzhang.objects.filter(pk=zuzhang.pk).update(
+ yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
+ )
+
+ # ========== 新增:组长每日统计(邀请管事 action=1) ==========
+ try:
+ update_zuzhang_daily_by_action(
+ yonghuid=zuzhang_yonghuid,
+ action=1 # 1 = 邀请管事
+ )
+ logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}")
+ except Exception as e:
+ logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}")
+
+
+ if not hasattr(user_main, 'dashou_profile'):
+ dashou_profile = UserDashou.objects.create(
+ user=user_main,
+ nicheng='大神',
+ chenghao='普通大神',
+ yaoqingren=zuzhang_yonghuid,
+ )
+ else:
+ dashou_profile = user_main.dashou_profile
+
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+
+ response_data = self._zhuangbei_fanhui_shuju(
+ user_main, dashou_profile, guanshi_profile, token
+ )
+
+ return Response({'code': 0, 'msg': '登录并注册成功!', 'data': response_data})
+
+ # ---------- 辅助方法(与接口1完全相同,完整列出以保证完整性) ----------
+
+ def _get_wechat_openid(self, code):
+ try:
+ appid = getattr(settings, 'WEIXIN_APPID', '')
+ secret = getattr(settings, 'WEIXIN_SECRET', '')
+ if not appid or not secret:
+ raise ValueError('微信配置未设置')
+ url = 'https://api.weixin.qq.com/sns/jscode2session'
+ params = {
+ 'appid': appid,
+ 'secret': secret,
+ 'js_code': code,
+ 'grant_type': 'authorization_code'
+ }
+ response = requests.get(url, params=params, timeout=10)
+ result = response.json()
+ if 'openid' in result:
+ return result
+ else:
+ errcode = result.get('errcode', 'unknown')
+ errmsg = result.get('errmsg', '未知错误')
+ return {'errmsg': f'[{errcode}]{errmsg}'}
+ except requests.exceptions.Timeout:
+ logger.error('请求微信接口超时')
+ return {'errmsg': '请求微信接口超时'}
+ except Exception as e:
+ logger.error(f'请求微信接口异常: {e}')
+ return {'errmsg': f'请求微信接口异常: {str(e)}'}
+
+ def _shengcheng_yonghu_id(self):
+ for _ in range(10):
+ timestamp_part = str(int(time.time()))[-5:].zfill(5)
+ random_part = str(random.randint(0, 99)).zfill(2)
+ user_id = timestamp_part + random_part
+ if len(user_id) == 7 and user_id.isdigit():
+ if not UserMain.objects.filter(yonghuid=user_id).exists():
+ return user_id
+ raise Exception('生成用户ID失败')
+
+ def _huoqu_kehuduan_ip(self, request):
+ 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
+ ip = request.META.get('REMOTE_ADDR', '')
+ return ip if ip else '0.0.0.0'
+
+ def _huoqu_qun_peizhi(self):
+ try:
+ qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2])
+ result = {
+ 'dashouqun': '',
+ 'dashouqunid': '',
+ 'guanshiqun': '',
+ 'guanshiqunid': ''
+ }
+ for config in qun_configs:
+ if config.id == 1:
+ result['dashouqun'] = config.neirong or ''
+ result['dashouqunid'] = config.qunid or ''
+ elif config.id == 2:
+ result['guanshiqun'] = config.neirong or ''
+ result['guanshiqunid'] = config.qunid or ''
+ return result
+ except Exception as e:
+ logger.error(f"获取群配置失败: {e}")
+ return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''}
+
+ def _zhuangbei_dashou_fanhui(self, dashou_profile):
+ data = {
+ 'dashounicheng': dashou_profile.nicheng,
+ 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
+ 'yongjin': str(dashou_profile.yue),
+ 'zonge': str(dashou_profile.zonge),
+ 'yajin': str(dashou_profile.yajin),
+ 'chenghao': dashou_profile.chenghao,
+ 'jinfen': dashou_profile.jifen,
+ 'chengjiaoliang': dashou_profile.chengjiaozongliang,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'dashouzhuangtai': dashou_profile.zhuangtai,
+ }
+ huiyuan_list = []
+ try:
+ records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid)
+ for record in records:
+ shifou_daoqi = record.jiance_shifou_daoqi()
+ huiyuan_list.append({
+ 'huiyuan_id': record.huiyuan_id,
+ 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
+ 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
+ 'shifou_daoqi': shifou_daoqi,
+ 'jieshao': record.jieshao
+ })
+ except Exception as e:
+ logger.warning(f"获取会员列表失败: {e}")
+ data['clumber'] = huiyuan_list
+ return data
+
+ def _fanhui_xianyou_guanshi_info(self, user_main):
+ guanshi_profile = user_main.guanshi_profile
+ dashou_profile = getattr(user_main, 'dashou_profile', None)
+ group_info = self._huoqu_qun_peizhi()
+ dashou_status = 1 if dashou_profile else 0
+ shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0
+ guanshi_status = 1
+ try:
+ nicheng = user_main.boss_profile.nickname or '微信用户'
+ except:
+ nicheng = '微信用户'
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+ data = {
+ 'token': token,
+ 'nicheng': nicheng,
+ 'uid': user_main.yonghuid,
+ 'touxiang': user_main.avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ **group_info,
+ }
+ guanshi_data = {
+ 'gszhstatus': guanshi_profile.zhuangtai,
+ 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
+ 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
+ 'fenyongtixian': str(guanshi_profile.yue),
+ 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
+ }
+ data.update(guanshi_data)
+ if dashou_profile:
+ dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
+ data.update(dashou_data)
+ else:
+ data.update({
+ 'dashounicheng': '',
+ 'zhanghaostatus': 0,
+ 'yongjin': '0.00',
+ 'zonge': '0.00',
+ 'yajin': '0.00',
+ 'chenghao': '',
+ 'jinfen': 0,
+ 'chengjiaoliang': 0,
+ 'zaixianzhuangtai': 0,
+ 'dashouzhuangtai': 0,
+ 'clumber': []
+ })
+ return Response({'code': 0, 'msg': '您已是管事', 'data': data})
+
+ def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token):
+ try:
+ nicheng = user_main.boss_profile.nickname or '微信用户'
+ except:
+ nicheng = '微信用户'
+ dashou_status = 1
+ shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0
+ guanshi_status = 1
+ group_info = self._huoqu_qun_peizhi()
+ data = {
+ 'token': token,
+ 'nicheng': nicheng,
+ 'uid': user_main.yonghuid,
+ 'touxiang': user_main.avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ **group_info,
+ }
+ guanshi_data = {
+ 'gszhstatus': guanshi_profile.zhuangtai,
+ 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
+ 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
+ 'fenyongtixian': str(guanshi_profile.yue),
+ 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
+ }
+ data.update(guanshi_data)
+ dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
+ data.update(dashou_data)
+ return data
+
+
+
+
+class GuanshiRegisterView(APIView):
+ """
+ 已登录用户注册管事接口
+ 路径: /api/yonghu/guanshizhuce
+ 方法: POST
+ 权限: JWT认证
+ 请求体: { "inviteCode": "组长邀请码" }
+ 成功返回 code: 200
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 获取并验证邀请码
+ yaoqingma = request.data.get('inviteCode', '').strip()
+ if not yaoqingma:
+ return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if len(yaoqingma) > 100:
+ return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ current_user = request.user
+
+ # 2. 检查用户是否已是管事
+ if hasattr(current_user, 'guanshi_profile'):
+ # 用户已是管事,直接返回现有信息
+ return self._fanhui_xianyou_guanshi_info(current_user)
+
+ # 3. 验证邀请码对应的组长
+ try:
+ zuzhang = UserZuzhang.objects.select_related('user').get(yaoqingma=yaoqingma)
+ except UserZuzhang.DoesNotExist:
+ return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND)
+
+ if zuzhang.zhuangtai != 1:
+ return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None},
+ status=status.HTTP_403_FORBIDDEN)
+
+ zuzhang_yonghuid = zuzhang.user.yonghuid
+
+ # 4. 事务内创建管事实体和打手(如果需要)
+ with transaction.atomic():
+ # 创建管事扩展表
+ guanshi_yaoqingma = CreateInvitationCode(str(current_user.yonghuid))
+ guanshi_profile = UserGuanshi.objects.create(
+ user=current_user,
+ yaoqingma=guanshi_yaoqingma,
+ yaoqingren=zuzhang_yonghuid,
+ )
+
+ # 更新组长的邀请总数
+ UserZuzhang.objects.filter(pk=zuzhang.pk).update(
+ yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
+ )
+
+ # ========== 新增:组长每日统计(邀请管事 action=1) ==========
+ try:
+ update_zuzhang_daily_by_action(
+ yonghuid=zuzhang_yonghuid,
+ action=1 # 1 = 邀请管事
+ )
+ logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}")
+ except Exception as e:
+ logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}")
+
+ # 检查用户是否已是打手,如果不是则创建
+ if not hasattr(current_user, 'dashou_profile'):
+ dashou_profile = UserDashou.objects.create(
+ user=current_user,
+ nicheng='大手子',
+ chenghao='普通大手',
+ yaoqingren=zuzhang_yonghuid,
+ )
+ else:
+ dashou_profile = current_user.dashou_profile
+
+ # 5. 生成新token
+ refresh = RefreshToken.for_user(current_user)
+ token = str(refresh.access_token)
+
+ # 6. 准备返回数据(包含所有字段)
+ response_data = self._zhuangbei_fanhui_shuju(
+ current_user, dashou_profile, guanshi_profile, token
+ )
+
+ return Response({'code': 200, 'msg': '注册成功!', 'data': response_data})
+
+ # ---------- 辅助方法 ----------
+
+ def _huoqu_qun_peizhi(self):
+ """获取群配置信息"""
+ try:
+ qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2])
+ result = {
+ 'dashouqun': '',
+ 'dashouqunid': '',
+ 'guanshiqun': '',
+ 'guanshiqunid': ''
+ }
+ for config in qun_configs:
+ if config.id == 1:
+ result['dashouqun'] = config.neirong or ''
+ result['dashouqunid'] = config.qunid or ''
+ elif config.id == 2:
+ result['guanshiqun'] = config.neirong or ''
+ result['guanshiqunid'] = config.qunid or ''
+ return result
+ except Exception as e:
+ logger.error(f"获取群配置失败: {e}")
+ return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''}
+
+ def _zhuangbei_dashou_fanhui(self, dashou_profile):
+ """组装打手信息(字段名与前端完全匹配)"""
+ data = {
+ 'dashounicheng': dashou_profile.nicheng,
+ 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
+ 'yongjin': str(dashou_profile.yue),
+ 'zonge': str(dashou_profile.zonge),
+ 'yajin': str(dashou_profile.yajin),
+ 'chenghao': dashou_profile.chenghao,
+ 'jinfen': dashou_profile.jifen,
+ 'chengjiaoliang': dashou_profile.chengjiaozongliang,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'dashouzhuangtai': dashou_profile.zhuangtai,
+ }
+
+ # 会员列表
+ huiyuan_list = []
+ try:
+ records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid)
+ for record in records:
+ shifou_daoqi = record.jiance_shifou_daoqi()
+ huiyuan_list.append({
+ 'huiyuan_id': record.huiyuan_id,
+ 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
+ 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
+ 'shifou_daoqi': shifou_daoqi,
+ 'jieshao': record.jieshao
+ })
+ except Exception as e:
+ logger.warning(f"获取会员列表失败: {e}")
+ data['clumber'] = huiyuan_list
+ return data
+
+ def _fanhui_xianyou_guanshi_info(self, user_main):
+ """用户已是管事时的返回"""
+ guanshi_profile = user_main.guanshi_profile
+ dashou_profile = getattr(user_main, 'dashou_profile', None)
+
+ group_info = self._huoqu_qun_peizhi()
+
+ dashou_status = 1 if dashou_profile else 0
+ shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0
+ guanshi_status = 1
+
+ try:
+ nicheng = user_main.boss_profile.nickname or '微信用户'
+ except:
+ nicheng = '微信用户'
+
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+
+ data = {
+ 'token': token,
+ 'nicheng': nicheng,
+ 'uid': user_main.yonghuid,
+ 'touxiang': user_main.avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ **group_info,
+ }
+
+ # 管事信息
+ guanshi_data = {
+ 'gszhstatus': guanshi_profile.zhuangtai,
+ 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
+ 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
+ 'fenyongtixian': str(guanshi_profile.yue),
+ 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
+ }
+ data.update(guanshi_data)
+
+ if dashou_profile:
+ dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
+ data.update(dashou_data)
+ else:
+ data.update({
+ 'dashounicheng': '',
+ 'zhanghaostatus': 0,
+ 'yongjin': '0.00',
+ 'zonge': '0.00',
+ 'yajin': '0.00',
+ 'chenghao': '',
+ 'jinfen': 0,
+ 'chengjiaoliang': 0,
+ 'zaixianzhuangtai': 0,
+ 'dashouzhuangtai': 0,
+ 'clumber': []
+ })
+
+ return Response({'code': 200, 'msg': '您已是管事', 'data': data})
+
+ def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token):
+ """组装完整返回数据(新注册)"""
+ try:
+ nicheng = user_main.boss_profile.nickname or '微信用户'
+ except:
+ nicheng = '微信用户'
+
+ dashou_status = 1
+ shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0
+ guanshi_status = 1
+
+ group_info = self._huoqu_qun_peizhi()
+
+ data = {
+ 'token': token,
+ 'nicheng': nicheng,
+ 'uid': user_main.yonghuid,
+ 'touxiang': user_main.avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ **group_info,
+ }
+
+ # 管事信息
+ guanshi_data = {
+ 'gszhstatus': guanshi_profile.zhuangtai,
+ 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
+ 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
+ 'fenyongtixian': str(guanshi_profile.yue),
+ 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
+ }
+ data.update(guanshi_data)
+
+ dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
+ data.update(dashou_data)
+
+ return data
+
+
+
+
+
+
+
+class ZuzhangZhuceView(APIView):
+ """
+ 组长注册/激活接口
+ POST /yonghu/zuzhangzhuce
+ 请求体: {"inviteCode": "xxx"}
+ 返回:
+ {
+ "code": 200,
+ "msg": "success",
+ "data": {
+ "ketixian": "0.00", # 可提现金额
+ "guanshiCount": 0, # 邀请的管事数量
+ "fenhongZonge": "0.00" # 分红总额
+ }
+ }
+ 或错误码
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ # 硬编码激活码(注意转义特殊字符)
+ ACTIVATION_CODE = "237huwehdw77e777eyw224fr"
+
+ def post(self, request):
+ user = request.user
+ invite_code = request.data.get('inviteCode', '').strip()
+
+ if not invite_code:
+ return Response({'code': 400, 'msg': '邀请码不能为空'}, status=400)
+
+ # 检查是否已是组长
+ try:
+ zuzhang = UserZuzhang.objects.select_related('user').get(user=user)
+ # 已注册,直接返回数据
+ data = self._build_response_data(zuzhang, user)
+ return Response({'code':200, 'msg': '已注册', 'data': data})
+ except UserZuzhang.DoesNotExist:
+ pass
+
+ # 比对激活码
+ if invite_code != self.ACTIVATION_CODE:
+ return Response({'code': 400, 'msg': '邀请码无效'}, status=400)
+
+ # 生成唯一组长邀请码:时间戳 + 用户ID后6位 + 随机字符
+ yaoqingma = self._generate_unique_yaoqingma(user.yonghuid)
+
+ # 创建组长扩展记录
+ try:
+ with transaction.atomic():
+ zuzhang = UserZuzhang.objects.create(
+ user=user,
+ yaoqingma=yaoqingma,
+ fenyong_zonge=0.00,
+ ketixian_jine=0.00,
+ yaoqing_zongshu=0,
+ jinri_fenyong=0.00,
+ jinyue_fenyong=0.00,
+ zhuangtai=1,
+ jinri_tixian=0.00,
+ ewai_tixian_xiane=0.00,
+ kaioi_ewai_tixian=False,
+ kaioi_ewai_fenhong=False,
+ ewai_fenhong_jine=0.00
+ )
+ except IntegrityError:
+ # 极低概率重复,重试一次
+ yaoqingma = self._generate_unique_yaoqingma(user.yonghuid, retry=True)
+ zuzhang = UserZuzhang.objects.create(
+ user=user,
+ yaoqingma=yaoqingma,
+ # 其他字段默认同上
+ fenyong_zonge=0.00,
+ ketixian_jine=0.00,
+ yaoqing_zongshu=0,
+ jinri_fenyong=0.00,
+ jinyue_fenyong=0.00,
+ zhuangtai=1,
+ jinri_tixian=0.00,
+ ewai_tixian_xiane=0.00,
+ kaioi_ewai_tixian=False,
+ kaioi_ewai_fenhong=False,
+ ewai_fenhong_jine=0.00
+ )
+
+ data = self._build_response_data(zuzhang, user)
+ return Response({'code':200, 'msg': '注册成功', 'data': data})
+
+ def _generate_unique_yaoqingma(self, yonghuid, retry=False):
+ """生成唯一邀请码:时间戳+用户ID后6位+随机2位字符"""
+ timestamp = str(int(time.time()))[-6:] # 取后6位
+ uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0')
+ rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2))
+ base = f"Z{timestamp}{uid_part}{rand_str}"
+ # 确保唯一性(若已存在则追加随机数,最多尝试3次)
+ for _ in range(3):
+ if not UserZuzhang.objects.filter(yaoqingma=base).exists():
+ return base
+ base = base + random.choice(string.digits)
+ # 极端情况,再附加时间戳毫秒
+ millis = str(time.time()).replace('.', '')[-4:]
+ return base + millis
+
+ def _build_response_data(self, zuzhang, user):
+ """组装前端所需数据"""
+ # 计算邀请的管事数量(通过UserGuanshi表的yaoqingren字段关联到user的yonghuid)
+ guanshi_count = UserGuanshi.objects.filter(yaoqingren=user.yonghuid).count()
+ return {
+ 'ketixian': str(zuzhang.ketixian_jine), # 可提现余额
+ 'guanshiCount': guanshi_count, # 邀请管事数
+ 'fenhongZonge': str(zuzhang.fenyong_zonge) # 分红总额
+ }
+
+
+
+class ZuzhangXinxiView(APIView):
+ """
+ 组长信息查询接口
+ POST /yonghu/zuzhangxinxi
+ 请求体: 无
+ 返回: 同注册接口成功返回格式
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ user = request.user
+ try:
+ zuzhang = UserZuzhang.objects.get(user=user)
+ except UserZuzhang.DoesNotExist:
+ return Response({'code': 404, 'msg': '用户不是组长'}, status=404)
+
+ guanshi_count = UserGuanshi.objects.filter(yaoqingren=user.yonghuid).count()
+ data = {
+ 'ketixian': str(zuzhang.ketixian_jine),
+ 'guanshiCount': guanshi_count,
+ 'fenhongZonge': str(zuzhang.fenyong_zonge)
+ }
+ return Response({'code': 200, 'msg': 'success', 'data': data})
+
+
+
+
+
+# apps/yonghu/views_dashou_jianquan.py
+"""
+打手 IM 消息权限鉴权接口
+URL: /yonghu/dshqltqx
+方法: POST
+认证: JWT Token (request.user)
+
+鉴权逻辑(满足任意一条即通过):
+ 1. 押金 >= 5 元 → 通过
+ 2. 拥有未过期会员 → 通过
+ 3. 存在订单状态为 2 或 8 的订单(作为接单打手) → 通过
+ 以上均不满足 → 拒绝
+"""
+
+
+class DashouJianquanView(APIView):
+ """打手消息权限鉴权"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # ========== 1. 通过 JWT 获取当前登录用户 ==========
+ user = request.user # UserMain 实例
+
+ # ========== 2. 检查是否为已注册打手 ==========
+ try:
+ dashou = user.dashou_profile # OneToOneField 反向查询
+ except UserDashou.DoesNotExist:
+ return Response({
+ 'code': 0,
+ 'allow': 0,
+ 'msg': '您尚未注册打手身份,请先注册'
+ })
+
+ # ========== 3. 条件一:押金 >= 5 元 ==========
+ if dashou.yajin and dashou.yajin >= 5:
+ return Response({
+ 'code': 0,
+ 'allow': 1,
+ 'msg': '鉴权通过(押金满足条件)'
+ })
+
+ # ========== 4. 条件二:存在未过期会员 ==========
+ # 高效查询:利用 yonghu_id + huiyuan_zhuangtai 复合索引
+ huiyuan_list = Huiyuangoumai.objects.filter(
+ yonghu_id=user.yonghuid,
+ huiyuan_zhuangtai=1 # 只查状态为"可用"的,减少内存遍历
+ ).only('id', 'daoqi_time', 'huiyuan_zhuangtai')
+
+ for hy in huiyuan_list:
+ if not hy.jiance_shifou_daoqi(): # 未过期返回 False
+ return Response({
+ 'code': 0,
+ 'allow': 1,
+ 'msg': '鉴权通过(会员未过期)'
+ })
+
+ # ========== 5. 条件三:存在订单状态为 2 或 8 的订单 ==========
+ # 利用 jiedan_dashou_id + zhuangtai 复合索引,只查 1 条
+ has_valid_order = Dingdan.objects.filter(
+ jiedan_dashou_id=user.yonghuid,
+ zhuangtai__in=[2, 8]
+ ).only('id').exists() # exists() 高性能判断,只发 SELECT 1 LIMIT 1
+
+ if has_valid_order:
+ return Response({
+ 'code': 0,
+ 'allow': 1,
+ 'msg': '鉴权通过(存在有效订单)'
+ })
+
+ # ========== 6. 以上均不满足 → 拒绝 ==========
+ return Response({
+ 'code': 0,
+ 'allow': 0,
+ 'msg': '消息权限不足,请充值会员或联系管理员'
+ })
+
+
+
+# apps/yonghu/views_laoban_jianquan.py
+"""
+老板(普通用户) IM 消息权限鉴权接口
+URL: /yonghu/lbhqltqx
+方法: POST
+认证: JWT Token (request.user)
+
+鉴权逻辑:
+ 查询平台订单扩展表(DingdanPingtai),
+ 通过关联的订单主表(Dingdan)查找该老板的订单,
+ 只要存在任意一条订单状态在 1~8 范围内 → 通过
+"""
+
+
+
+class LaobanJianquanView(APIView):
+ """老板消息权限鉴权"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # ========== 1. 通过 JWT 获取当前登录用户 ==========
+ user = request.user # UserMain 实例
+
+ # ========== 2. 高效查询平台订单扩展表 ==========
+ # 利用 DingdanPingtai.laoban_id 索引 + Dingdan.zhuangtai 索引
+ # 只查 1 条,用 exists() 实现最高性能
+ has_valid_order = DingdanPingtai.objects.filter(
+ laoban_id=user.yonghuid
+ ).filter(
+ dingdan__zhuangtai__in=[1, 2, 3, 4, 5, 6, 7, 8]
+ ).only('id').exists()
+
+ if has_valid_order:
+ return Response({
+ 'code': 0,
+ 'allow': 1,
+ 'msg': '鉴权通过'
+ })
+
+ return Response({
+ 'code': 0,
+ 'allow': 0,
+ 'msg': '您暂无订单记录,消息功能暂不可用'
+ })
+
+
+
+
+
+
+
+# views.py (在处罚相关app的views中)
+class FaKuanJiFenTongJiView(APIView):
+ """
+ 获取罚款与积分处罚统计总览
+ 路径: POST /yonghu/cffktjhq
+ """
+ permission_classes = [IsAuthenticated, DashouPermission]
+
+ def post(self, request):
+ try:
+ user_main = request.user
+ yonghuid = user_main.yonghuid
+
+ # ================= 罚款统计 =================
+ fadan_stats = Fadan.objects.filter(beichufa_id=yonghuid).aggregate(
+ total=Count('id'),
+ daijiaona=Count('id', filter=Q(zhuangtai=1)),
+ shensuzhong=Count('id', filter=Q(zhuangtai=3)),
+ yijiaona=Count('id', filter=Q(zhuangtai=2)),
+ yibohui=Count('id', filter=Q(zhuangtai=4)),
+ )
+
+ # ================= 积分处罚统计 =================
+ jifen_stats = Chufajilu.objects.filter(dashouid=yonghuid).aggregate(
+ total=Count('id'),
+ daichuli=Count('id', filter=Q(sqzhuangtai=0) | Q(sqzhuangtai=3)),
+ yichuli=Count('id', filter=Q(sqzhuangtai=1) | Q(sqzhuangtai=2)),
+ # 细分
+ daichuli_0=Count('id', filter=Q(sqzhuangtai=0)),
+ shensuzhong_3=Count('id', filter=Q(sqzhuangtai=3)),
+ yichufa_1=Count('id', filter=Q(sqzhuangtai=1)),
+ yibohui_2=Count('id', filter=Q(sqzhuangtai=2)),
+ )
+
+ data = {
+ "fakuan": {
+ "total": fadan_stats['total'],
+ "daijiaona": fadan_stats['daijiaona'],
+ "shensuzhong": fadan_stats['shensuzhong'],
+ "yijiaona": fadan_stats['yijiaona'],
+ "yibohui": fadan_stats['yibohui'],
+ },
+ "jifen": {
+ "total": jifen_stats['total'],
+ "daichuli": jifen_stats['daichuli'],
+ "yichuli": jifen_stats['yichuli'],
+ "daichuli_0": jifen_stats['daichuli_0'],
+ "shensuzhong_3": jifen_stats['shensuzhong_3'],
+ "yichufa_1": jifen_stats['yichufa_1'],
+ "yibohui_2": jifen_stats['yibohui_2'],
+ }
+ }
+
+ return Response({'code': 0, 'msg': '获取统计成功', 'data': data})
+
+ except Exception as e:
+ logger.error(f"获取统计异常 {yonghuid}: {e}", exc_info=True)
+ return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
+
+class DaShouFaKuanLieBiaoView(APIView):
+ """
+ 打手罚款列表接口
+ 路径:POST /yonghu/dsfklbhq
+ 参数:
+ page: 页码(默认1)
+ page_size: 每页条数(默认10,最大50)
+ zhuangtai: 状态筛选(1待缴纳/2已缴纳/3申诉中/4申诉成功,0或不传表示全部)
+ sousuo_dingdan_id: 订单号模糊搜索
+ 返回:{
+ list: [{
+ id, beichufa_id, guanliandingdan_id, chufaliyou, fakuanjine, zhuangtai,
+ yingxiang_qiangdan, shensuliyou, bohuiliyou,
+ zhengju_tupian: [...], // 证据图片(yongtu=1)
+ shensu_tupian: [...], // 申诉图片(yongtu=2)
+ create_time, update_time
+ }],
+ has_more, page, page_size
+ }
+ """
+ permission_classes = [IsAuthenticated, DashouPermission]
+
+ def post(self, request):
+ try:
+ user_main = request.user
+ yonghuid = user_main.yonghuid
+
+ # 获取分页参数
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 10))
+ if page_size > 50:
+ page_size = 50
+ if page_size < 1:
+ page_size = 10
+
+ # 状态筛选
+ zhuangtai = request.data.get('zhuangtai')
+ # 订单号搜索
+ sousuo_dingdan_id = request.data.get('sousuo_dingdan_id', '').strip()
+
+ # 基础查询:当前用户的罚单
+ qs = Fadan.objects.filter(beichufa_id=yonghuid)
+
+ # 状态筛选
+ if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]:
+ qs = qs.filter(zhuangtai=int(zhuangtai))
+
+ # 订单号模糊搜索
+ if sousuo_dingdan_id:
+ qs = qs.filter(guanliandingdan_id__icontains=sousuo_dingdan_id)
+
+ # 按创建时间倒序
+ qs = qs.order_by('-create_time')
+
+ # 分页
+ paginator = Paginator(qs, page_size)
+ try:
+ page_obj = paginator.page(page)
+ except Exception:
+ return Response({
+ 'code': 0, 'msg': '获取成功',
+ 'data': {'list': [], 'has_more': False, 'page': page, 'page_size': page_size}
+ })
+
+ records = list(page_obj)
+ fadan_ids = [r.id for r in records]
+
+ # 批量获取图片,按用途分组
+ zhengju_map = {} # key: fadan_id, value: [url, ...]
+ shensu_map = {}
+ if fadan_ids:
+ tupians = FadanShensuTupian.objects.filter(
+ fadan_id__in=fadan_ids
+ ).values('fadan_id', 'tupian_url', 'yongtu')
+ for t in tupians:
+ fid = t['fadan_id']
+ url = t['tupian_url']
+ if t['yongtu'] == 1:
+ zhengju_map.setdefault(fid, []).append(url)
+ else:
+ shensu_map.setdefault(fid, []).append(url)
+
+ # 组装返回数据
+ chufa_list = []
+ for r in records:
+ rid = r.id
+ item = {
+ 'id': rid,
+ 'beichufa_id': r.beichufa_id,
+ 'guanliandingdan_id': r.guanliandingdan_id or '',
+ 'chufaliyou': r.chufaliyou or '',
+ 'fakuanjine': str(r.fakuanjine),
+ 'zhuangtai': r.zhuangtai,
+ 'yingxiang_qiangdan': r.yingxiang_qiangdan,
+ 'shensuliyou': r.shensuliyou or '',
+ 'bohuiliyou': r.bohuiliyou or '',
+ 'zhengju_tupian': zhengju_map.get(rid, []), # 证据图片
+ 'shensu_tupian': shensu_map.get(rid, []), # 申诉图片
+ 'create_time': r.create_time.strftime('%Y-%m-%d %H:%M:%S') if r.create_time else '',
+ 'update_time': r.update_time.strftime('%Y-%m-%d %H:%M:%S') if r.update_time else '',
+ }
+ chufa_list.append(item)
+
+ has_more = page_obj.has_next()
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': {
+ 'list': chufa_list,
+ 'has_more': has_more,
+ 'page': page,
+ 'page_size': page_size,
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"获取罚款列表异常 {yonghuid if 'yonghuid' in locals() else ''}: {e}", exc_info=True)
+ return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
+
+
+class FaKuanShenSuView(APIView):
+ """
+ 罚款申诉提交/修改接口
+ 路径:POST /yonghu/fkss
+ 参数:
+ fadan_id: 罚单ID
+ shensu_liyou: 申诉理由
+ shensu_tupian_urls: 图片相对URL数组(可空数组,但至少要有理由)
+ 逻辑:
+ - 仅待缴纳状态且无驳回理由的罚单可申诉
+ - 允许对已有申诉进行修改:先删除旧申诉图片(yongtu=2),再添加新图片
+ """
+ permission_classes = [IsAuthenticated, DashouPermission]
+
+ def post(self, request):
+ try:
+ user_main = request.user
+ yonghuid = user_main.yonghuid
+ fadan_id = request.data.get('fadan_id')
+ shensu_liyou = request.data.get('shensu_liyou', '').strip()
+ tupian_urls = request.data.get('shensu_tupian_urls', [])
+
+ if not fadan_id:
+ return Response({'code': 400, 'msg': '缺少罚款ID'}, status=400)
+ if not shensu_liyou:
+ return Response({'code': 400, 'msg': '请输入申诉理由'}, status=400)
+
+ # 查询罚单,确保属于当前打手
+ try:
+ fadan = Fadan.objects.get(id=fadan_id, beichufa_id=yonghuid)
+ except Fadan.DoesNotExist:
+ return Response({'code': 404, 'msg': '罚单不存在或无权操作'}, status=404)
+
+ # 只允许待缴纳状态(状态1)且没有驳回理由的罚单申诉
+ if fadan.zhuangtai != 1:
+ return Response({'code': 400, 'msg': '当前状态不可申诉'}, status=400)
+ if fadan.bohuiliyou:
+ return Response({'code': 400, 'msg': '该罚单申诉已被驳回,不可再次申诉'}, status=400)
+
+ # 更新罚单状态为申诉中,并记录理由
+ fadan.zhuangtai = 3
+ fadan.shensuliyou = shensu_liyou
+ fadan.save()
+
+ # 删除旧的申诉图片(yongtu=2),仅删除打手自己上传的申诉图
+ FadanShensuTupian.objects.filter(fadan=fadan, yongtu=2).delete()
+
+ # 插入新上传的申诉图片
+ for url in tupian_urls:
+ FadanShensuTupian.objects.create(
+ fadan=fadan,
+ beichufa_id=yonghuid,
+ tupian_url=url,
+ yongtu=2
+ )
+
+ return Response({'code': 0, 'msg': '申诉提交成功'})
+
+ except Exception as e:
+ logger.error(f"罚款申诉异常 {yonghuid if 'yonghuid' in locals() else ''}: {e}", exc_info=True)
+ return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
+
+
+
+
+
+
+
+# 需要替换为你自己项目中的导入路径
+
+# XIAOCHENGXU LOGGER?
+# ==================== 1. 罚款支付下单接口 ====================
+
+class FaKuanPayView(APIView):
+ """
+ 罚款微信支付下单接口(完全照抄 YajinGoumai)
+ 路径:POST /yonghu/fkjn
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ fadan_id = request.data.get('fadan_id')
+ if not fadan_id:
+ return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ fadan = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid)
+ except Fadan.DoesNotExist:
+ return Response({'code': 404, 'message': '罚单不存在或不属于您'}, status=status.HTTP_404_NOT_FOUND)
+
+ if fadan.zhuangtai not in [1, 3]:
+ return Response({'code': 400, 'message': '罚单当前状态不可支付'}, status=status.HTTP_400_BAD_REQUEST)
+
+ jine = float(fadan.fakuanjine)
+
+ timestamp_ms = int(time.time() * 1000)
+ timestamp_ns = int(time.time_ns() // 1000) % 1000000
+ random_num = random.randint(0, 99)
+ dingdanid = f"FK{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
+
+ chongzhi_jilu = Czjilu.objects.create(
+ dingdan_id=dingdanid,
+ zhuangtai=9,
+ yonghuid=request.user.yonghuid,
+ jine=jine,
+ leixing=4,
+ shuoming=settings.FAKUAN_PAY_DESCRIPTION
+ )
+
+ try:
+ pay_params = self.generate_wechat_pay_params(
+ dingdanid=dingdanid,
+ jine=jine,
+ openid=request.user.openid,
+ pay_type='fakuan'
+ )
+ except Exception as e:
+ chongzhi_jilu.delete()
+ logger.error(f"生成微信支付参数失败: {str(e)}")
+ return Response({
+ 'code': 500,
+ 'message': f'支付参数生成失败: {str(e)}'
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ return Response({
+ 'code': 200,
+ 'message': '支付参数生成成功',
+ 'payParams': pay_params,
+ 'dingdanid': dingdanid
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f"罚款支付下单接口异常: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 500,
+ 'message': '服务器内部错误,请稍后重试'
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
+ APPID = settings.WEIXIN_APPID
+ MCHID = settings.WEIXIN_MCHID
+ KEY = settings.WEIXIN_SHANGHUMIYAO
+
+ if pay_type == 'fakuan':
+ PAY_DESCRIPTION = settings.FAKUAN_PAY_DESCRIPTION
+ NOTIFY_URL = settings.FAKUAN_PAY_NOTIFY_URL # 回调地址在这里
+ else:
+ raise Exception('未知的支付类型')
+
+ nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+ total_fee = int(jine * 100)
+ trade_type = 'JSAPI'
+ user_ip = self.get_client_ip()
+
+ # 第一次签名
+ sign_data = {
+ 'appid': APPID,
+ 'mch_id': MCHID,
+ 'nonce_str': nonce_str,
+ 'body': PAY_DESCRIPTION,
+ 'out_trade_no': dingdanid,
+ 'total_fee': total_fee,
+ 'spbill_create_ip': user_ip,
+ 'notify_url': NOTIFY_URL, # 关键:把回调地址传入
+ 'trade_type': trade_type,
+ 'openid': openid
+ }
+
+ sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
+ sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
+ sign_string += f'&key={KEY}'
+ sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
+
+ # 构建 XML,这里必须包含 notify_url
+ xml_data = f"""
+{APPID}
+{PAY_DESCRIPTION}
+{MCHID}
+{nonce_str}
+{NOTIFY_URL}
+{openid}
+{dingdanid}
+{user_ip}
+{total_fee}
+{trade_type}
+{sign}
+"""
+
+ response = requests.post(
+ 'https://api.mch.weixin.qq.com/pay/unifiedorder',
+ data=xml_data.encode('utf-8'),
+ headers={'Content-Type': 'application/xml'},
+ timeout=10
+ )
+
+ root = ET.fromstring(response.content)
+ return_code = root.find('return_code').text
+ result_code = root.find('result_code').text
+
+ if return_code == 'SUCCESS' and result_code == 'SUCCESS':
+ prepay_id = root.find('prepay_id').text
+ timestamp = str(int(time.time()))
+ nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+ package = f'prepay_id={prepay_id}'
+ sign_type = 'MD5'
+
+ pay_sign_data = {
+ 'appId': APPID,
+ 'timeStamp': timestamp,
+ 'nonceStr': nonce_str,
+ 'package': package,
+ 'signType': sign_type
+ }
+
+ sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
+ pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
+ pay_sign_string += f'&key={KEY}'
+ pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
+
+ return {
+ 'appId': APPID,
+ 'timeStamp': timestamp,
+ 'nonceStr': nonce_str,
+ 'package': package,
+ 'signType': sign_type,
+ 'paySign': pay_sign
+ }
+ else:
+ err_code = root.find('err_code')
+ err_code_des = root.find('err_code_des')
+ error_details = f"return_code: {return_code}"
+ if err_code is not None:
+ error_details += f", err_code: {err_code.text}"
+ if err_code_des is not None:
+ error_details += f", err_code_des: {err_code_des.text}"
+ raise Exception(f"微信支付下单失败: {error_details}")
+
+ def get_client_ip(self):
+ request = self.request
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ if x_forwarded_for:
+ ip = x_forwarded_for.split(',')[0]
+ else:
+ ip = request.META.get('REMOTE_ADDR')
+ return ip if ip else '127.0.0.1'
+
+
+# ==================== 2. 罚款支付回调接口 ====================
+class FaKuanHuitiaoView(View):
+
+ """
+ 罚款支付回调接口(严格借鉴 YajinHuitiao 接口,参数一个不少)
+ 路径:POST /yonghu/fk_huitiao
+ """
+ def post(self, request):
+ try:
+ # 1. 解析微信回调XML数据
+ request_body = request.body
+ logger.info(f"收到微信支付回调,原始数据: {request_body.decode('utf-8')}")
+
+ root = ET.fromstring(request_body)
+
+ # 获取关键字段
+ return_code = root.find('return_code').text
+ result_code = root.find('result_code').text
+ out_trade_no = root.find('out_trade_no').text # 订单ID
+ transaction_id = root.find('transaction_id').text # 微信交易号
+ total_fee = int(root.find('total_fee').text) # 支付金额(分)
+
+ logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分")
+
+ # 2. 验证基本返回状态
+ if return_code != 'SUCCESS' or result_code != 'SUCCESS':
+ logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}")
+ return self.wechat_response('FAIL', '支付失败')
+
+ # 3. 验证签名(确保是微信发来的)
+ if not self.verify_signature(request_body):
+ logger.error(f"签名验证失败: {out_trade_no}")
+ return self.wechat_response('FAIL', '签名验证失败')
+
+ # 4. 查询订单
+ try:
+ order = Czjilu.objects.get(dingdan_id=out_trade_no)
+ logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}")
+ except Czjilu.DoesNotExist:
+ logger.error(f"订单不存在: {out_trade_no}")
+ return self.wechat_response('FAIL', '订单不存在')
+
+ # 5. 检查订单状态是否为未支付(9)
+ if order.zhuangtai != 9:
+ logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
+ # 已处理过的订单直接返回成功,避免微信重复回调
+ return self.wechat_response('SUCCESS', 'OK')
+
+ # 6. 验证金额(微信返回的是分,需要转换)
+ wechat_amount_yuan = total_fee / 100 # 转换为元
+ order_amount_yuan = float(order.jine) # 订单金额(元)
+
+ # 允许1分钱的误差(浮点数比较)
+ if abs(wechat_amount_yuan - order_amount_yuan) > 0.01:
+ logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
+ return self.wechat_response('FAIL', '金额不一致')
+
+ # 7. 更新订单状态为已支付(3)
+ order.zhuangtai = 3
+ order.save(update_fields=['zhuangtai'])
+ logger.info(f"订单状态更新成功: {order.dingdan_id} -> 3")
+
+ # 8. 更新罚单状态为已缴纳(2)
+ try:
+ # 根据订单中的用户ID和金额匹配待缴纳的罚单
+ fadan = Fadan.objects.filter(
+ beichufa_id=order.yonghuid,
+ fakuanjine=order.jine,
+ zhuangtai__in=[1, 3]
+ ).first()
+ if fadan:
+ fadan.zhuangtai = 2 # 已缴纳
+ fadan.save(update_fields=['zhuangtai'])
+ logger.info(f"罚单 {fadan.id} 已自动标记为已缴纳")
+
+ success, msg = process_fadan_fenhong(fadan.id)
+ if not success:
+ logger.error(f"罚单 {fadan.id} 分红处理失败: {msg}")
+ else:
+ logger.info(f"罚单 {fadan.id} 分红处理完成: {msg}")
+
+ except Exception as e:
+ logger.error(f"更新罚单状态失败: {str(e)}", exc_info=True)
+
+ # 9. 更新收支记录表
+ try:
+
+ # 获取订单金额(确保是Decimal类型)
+ jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
+
+ update_daily_income(jine_decimal)
+
+ # 查询或创建ID=1的收支记录
+ szjilu, created = Szjilu.objects.get_or_create(
+ id=1,
+ defaults={
+ 'zongsy': Decimal('0.00'),
+ 'zongls': Decimal('0.00'),
+ 'zongzc': Decimal('0.00'),
+ 'jrzc': Decimal('0.00'),
+ 'jrls': Decimal('0.00')
+ }
+ )
+
+ # 更新三个字段
+ szjilu.zongsy += jine_decimal # 总收益
+ szjilu.zongls += jine_decimal # 总流水
+ szjilu.jrls += jine_decimal # 今日流水
+
+ szjilu.save()
+
+ logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元")
+
+ except Exception as e:
+ # 收支记录更新失败不影响主流程,只记录日志
+ logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
+
+ # 10. 返回成功响应给微信
+ logger.info(f"罚款支付回调处理完成: {out_trade_no}")
+ return self.wechat_response('SUCCESS', 'OK')
+
+ except ET.ParseError as e:
+ logger.error(f"XML解析失败: {str(e)}")
+ return self.wechat_response('FAIL', '数据格式错误')
+ except Exception as e:
+ logger.error(f"罚款支付回调处理异常: {str(e)}", exc_info=True)
+ # 返回FAIL让微信重试
+ return self.wechat_response('FAIL', '处理异常')
+
+ def verify_signature(self, xml_data):
+ """验证微信支付签名(完全照搬 YajinHuitiao 的 verify_signature)"""
+ try:
+ root = ET.fromstring(xml_data)
+ sign = root.find('sign').text
+
+ if not sign:
+ logger.error("签名字段不存在")
+ return False
+
+ # 获取所有参数并排除sign
+ params = {}
+ for child in root:
+ if child.tag != 'sign' and child.text is not None:
+ params[child.tag] = child.text
+
+ # 按照参数名ASCII码从小到大排序
+ sorted_params = sorted(params.items(), key=lambda x: x[0])
+
+ # 拼接字符串
+ sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
+ sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
+
+ # 计算签名
+ calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
+
+ is_valid = sign == calculated_sign
+ if not is_valid:
+ logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}")
+
+ return is_valid
+
+ except Exception as e:
+ logger.error(f"验证签名异常: {str(e)}")
+ return False
+
+ def wechat_response(self, return_code, return_msg):
+ """返回微信支付回调响应(完全照搬 YajinHuitiao 的 wechat_response)"""
+ xml_response = f"""
+
+
+"""
+
+ logger.info(f"返回微信响应: {return_code} - {return_msg}")
+ return HttpResponse(xml_response, content_type='application/xml')
+
+
+# ==================== 3. 支付轮询接口 ====================
+class FaKuanPayPollView(APIView):
+ """罚款支付轮询接口(借鉴 YajinLunxun)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ dingdanid = request.data.get('dingdanid')
+ if not dingdanid:
+ return Response({'code': 400, 'message': '订单ID不能为空'})
+
+ order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid)
+ if order.zhuangtai == 3:
+ return Response({'code': 200, 'message': '支付成功'})
+ else:
+ return Response({'code': 400, 'message': '订单尚未支付'})
+ except Czjilu.DoesNotExist:
+ return Response({'code': 400, 'message': '订单不存在'})
+ except Exception as e:
+ logger.error(f"轮询异常: {str(e)}")
+ return Response({'code': 500, 'message': '服务器内部错误'})
+
+
+# ==================== 4. 支付失败处理接口 ====================
+class FaKuanPayFailView(APIView):
+ """罚款支付失败处理接口(借鉴 YajinShibai)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ dingdanid = request.data.get('dingdanid')
+ if not dingdanid:
+ return Response({'code': 400, 'message': '订单ID不能为空'})
+
+ order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid)
+ if order.zhuangtai == 9: # 未支付
+ order.delete()
+ logger.info(f"用户{request.user.yonghuid}删除未支付订单: {dingdanid}")
+ return Response({'code': 200, 'message': '订单已删除'})
+ else:
+ return Response({'code': 400, 'message': '订单状态不是未支付,无法删除'})
+ except Czjilu.DoesNotExist:
+ return Response({'code': 400, 'message': '订单不存在'})
+ except Exception as e:
+ logger.error(f"支付失败处理异常: {str(e)}")
+ return Response({'code': 500, 'message': '服务器内部错误'})
+
+
+
+
+
+
+class KaohePayView(APIView):
+ """
+ 考核支付下单接口
+ 路径:POST /yonghu/khzf
+ 参数:
+ tag_id: 标签ID
+ game_nick: 游戏昵称
+ remark: 备注
+ reapply: 是否再次申请 (true/false)
+ jilu_id: 再次申请时的原记录ID(reapply=true时必传)
+ shenheguan_id: 指定考核官ID(可选)
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ user = request.user
+ tag_id = request.data.get('tag_id')
+ game_nick = request.data.get('game_nick', '').strip()
+ remark = request.data.get('remark', '').strip()
+ reapply = request.data.get('reapply', False)
+ jilu_id = request.data.get('jilu_id') if reapply else None
+ shenheguan_id = request.data.get('shenheguan_id', '').strip()
+
+ logger.info(f"用户 {user.yonghuid} 发起考核支付申请,标签ID: {tag_id}, 再次申请: {reapply}")
+
+ if not tag_id or not game_nick:
+ logger.warning(f"用户 {user.yonghuid} 参数不完整: tag_id={tag_id}, game_nick={game_nick}")
+ return Response({'code': 400, 'message': '标签ID和游戏昵称不能为空'})
+
+ # 1. 检查是否有未支付的考核订单
+ """unpaid_order = Czjilu.objects.filter(
+ yonghuid=user.yonghuid,
+ leixing=5,
+ zhuangtai=9
+ ).exists()
+ if unpaid_order:
+ logger.warning(f"用户 {user.yonghuid} 存在未支付的考核订单")
+ return Response({'code': 400, 'message': '您有未支付的考核订单,请先完成支付或取消'})"""
+
+ # 2. 验证标签
+ try:
+ chenghao = Chenghao.objects.get(id=tag_id)
+ logger.info(f"标签验证通过: {chenghao.mingcheng}")
+ except Chenghao.DoesNotExist:
+ logger.error(f"标签不存在: tag_id={tag_id}")
+ return Response({'code': 404, 'message': '标签不存在'})
+
+ # 3. 检查用户是否已拥有该标签
+ if YonghuChenghao.objects.filter(yonghu=user, chenghao=chenghao).exists():
+ logger.warning(f"用户 {user.yonghuid} 已拥有标签 {chenghao.mingcheng}")
+ return Response({'code': 400, 'message': '您已拥有该标签,无需重复考核'})
+
+ # 4. 检查是否有未完成的申请
+ if ShenheJilu.objects.filter(
+ shenqingren_id=user.yonghuid,
+ chenghao=chenghao,
+ zhuangtai__in=[0, 3] # 审核中、待审核
+ ).exists():
+ logger.warning(f"用户 {user.yonghuid} 有未完成的考核申请")
+ return Response({'code': 400, 'message': '您有未完成的考核申请,请等待结果'})
+
+ # 5. 计算考核次数和费用
+ if reapply and jilu_id:
+ last = ShenheJilu.objects.filter(
+ jilu_id=jilu_id,
+ shenqingren_id=user.yonghuid,
+ zhuangtai=2
+ ).first()
+ if not last:
+ logger.warning(f"用户 {user.yonghuid} 再次申请失败:原记录不存在或状态不是未通过,jilu_id={jilu_id}")
+ return Response({'code': 400, 'message': '原记录不存在或状态不是未通过'})
+ new_cishu = last.cishu + 1
+ logger.info(f"再次申请,原记录ID: {jilu_id},新次数: {new_cishu}")
+ else:
+ max_cishu = ShenheJilu.objects.filter(
+ shenqingren_id=user.yonghuid,
+ chenghao=chenghao,
+ zhuangtai__in=[1, 2]
+ ).aggregate(max=models.Max('cishu'))['max'] or 0
+ new_cishu = max_cishu + 1
+ logger.info(f"首次申请,计算次数: {new_cishu}")
+
+ # 6. 获取本次费用
+ fee = get_tag_fee(chenghao, new_cishu)
+ logger.info(f"本次考核费用: {fee}元,次数: {new_cishu}")
+
+ if fee <= 0:
+ logger.warning(f"费用为0,应使用免费申请接口,tag_id={tag_id}, cishu={new_cishu}")
+ return Response({'code': 400, 'message': '本次费用为0,请直接使用免费申请接口'})
+
+ # 7. 生成订单ID
+ timestamp_ms = int(time.time() * 1000)
+ timestamp_ns = int(time.time_ns() // 1000) % 1000000
+ random_num = random.randint(0, 99)
+ dingdanid = f"KH{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
+ logger.info(f"生成订单ID: {dingdanid}")
+
+ # 8. 创建支付订单(Czjilu)
+ order = Czjilu.objects.create(
+ dingdan_id=dingdanid,
+ zhuangtai=9, # 未支付
+ yonghuid=user.yonghuid,
+ jine=float(fee),
+ leixing=5, # 5-考核支付
+ shuoming=settings.KAOHE_PAY_DESCRIPTION
+ )
+ logger.info(f"创建支付订单成功: {dingdanid}")
+
+ # 9. 保存考核支付临时上下文(用于回调时取业务参数)
+ try:
+ KaohePayTemp.objects.create(
+ dingdan_id=dingdanid,
+ yonghuid=user.yonghuid,
+ tag_id=tag_id,
+ game_nick=game_nick,
+ remark=remark,
+ reapply=reapply,
+ jilu_id=jilu_id if reapply else None,
+ shenheguan_id=shenheguan_id,
+ cishu=new_cishu,
+ fee=fee,
+ )
+ logger.info(f"保存临时上下文成功: {dingdanid}")
+ except Exception as e:
+ logger.error(f"保存临时上下文失败: {str(e)}", exc_info=True)
+ # 临时表保存失败应该回滚订单
+ order.delete()
+ return Response({'code': 500, 'message': '系统异常,请稍后重试'})
+
+ # 10. 构造 attach 参数(只传订单ID,确保不超127字节)
+ attach_str = json.dumps({'oid': dingdanid}, ensure_ascii=False)
+ logger.info(f"attach内容: {attach_str},长度: {len(attach_str.encode('utf-8'))}字节")
+
+ # 11. 生成微信支付参数
+ try:
+ pay_params = self.generate_wechat_pay_params(
+ dingdanid=dingdanid,
+ jine=float(fee),
+ openid=user.openid,
+ attach=attach_str,
+ pay_type='kaohe'
+ )
+ logger.info(f"微信支付参数生成成功: {dingdanid}")
+ except Exception as e:
+ order.delete() # 下单失败,删除订单记录
+ # 同时删除临时表记录
+ try:
+ KaohePayTemp.objects.filter(dingdan_id=dingdanid).delete()
+ except Exception as del_e:
+ logger.error(f"删除临时表记录失败: {str(del_e)}")
+ logger.error(f"生成微信支付参数失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'message': f'支付参数生成失败: {str(e)}'})
+
+ return Response({
+ 'code': 200,
+ 'message': '支付参数生成成功',
+ 'payParams': pay_params,
+ 'dingdanid': dingdanid
+ })
+
+ except Exception as e:
+ logger.error(f"考核支付下单异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'message': '服务器内部错误'})
+
+ def generate_wechat_pay_params(self, dingdanid, jine, openid, attach, pay_type):
+ """生成微信JSAPI支付参数"""
+ APPID = settings.WEIXIN_APPID
+ MCHID = settings.WEIXIN_MCHID
+ KEY = settings.WEIXIN_SHANGHUMIYAO
+
+ if pay_type == 'kaohe':
+ PAY_DESCRIPTION = settings.KAOHE_PAY_DESCRIPTION
+ NOTIFY_URL = settings.KAOHE_PAY_NOTIFY_URL
+ else:
+ raise Exception('未知支付类型')
+
+ nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+ total_fee = int(jine * 100)
+ trade_type = 'JSAPI'
+ user_ip = self.get_client_ip()
+
+ logger.info(f"微信支付下单参数: out_trade_no={dingdanid}, total_fee={total_fee}, openid={openid}, ip={user_ip}")
+
+ # 第一次签名(统一下单)
+ sign_data = {
+ 'appid': APPID,
+ 'mch_id': MCHID,
+ 'nonce_str': nonce_str,
+ 'body': PAY_DESCRIPTION,
+ 'out_trade_no': dingdanid,
+ 'total_fee': total_fee,
+ 'spbill_create_ip': user_ip,
+ 'notify_url': NOTIFY_URL,
+ 'trade_type': trade_type,
+ 'openid': openid,
+ 'attach': attach,
+ }
+
+ sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
+ sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
+ sign_string += f'&key={KEY}'
+ sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
+
+ # 构建XML请求体
+ xml_data = f"""
+{APPID}
+{PAY_DESCRIPTION}
+{MCHID}
+{nonce_str}
+{NOTIFY_URL}
+{openid}
+{dingdanid}
+{user_ip}
+{total_fee}
+{trade_type}
+
+{sign}
+"""
+
+ logger.info(f"微信支付请求XML: {xml_data[:500]}...") # 只打印前500字符
+
+ response = requests.post(
+ 'https://api.mch.weixin.qq.com/pay/unifiedorder',
+ data=xml_data.encode('utf-8'),
+ headers={'Content-Type': 'application/xml'},
+ timeout=10
+ )
+
+ logger.info(f"微信支付响应: {response.text}")
+
+ root = ET.fromstring(response.content)
+
+ # 安全获取return_code
+ return_code_node = root.find('return_code')
+ if return_code_node is None or return_code_node.text is None:
+ logger.error(f"微信返回缺少return_code节点,原始内容: {response.text}")
+ raise Exception('微信支付接口异常,缺少return_code')
+ return_code = return_code_node.text
+
+ # 安全获取result_code
+ result_code_node = root.find('result_code')
+ if result_code_node is None or result_code_node.text is None:
+ logger.error(f"微信返回缺少result_code节点,原始内容: {response.text}")
+ raise Exception('微信支付接口异常,缺少result_code')
+ result_code = result_code_node.text
+
+ if return_code == 'SUCCESS' and result_code == 'SUCCESS':
+ prepay_id_node = root.find('prepay_id')
+ if prepay_id_node is None or prepay_id_node.text is None:
+ logger.error(f"统一下单成功但未返回prepay_id,原始内容: {response.text}")
+ raise Exception('微信支付接口异常,缺少prepay_id')
+ prepay_id = prepay_id_node.text
+
+ timestamp = str(int(time.time()))
+ nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+ package = f'prepay_id={prepay_id}'
+ sign_type = 'MD5'
+
+ # 第二次签名(前端调起支付用)
+ pay_sign_data = {
+ 'appId': APPID,
+ 'timeStamp': timestamp,
+ 'nonceStr': nonce_str,
+ 'package': package,
+ 'signType': sign_type,
+ }
+ sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
+ pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
+ pay_sign_string += f'&key={KEY}'
+ pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
+
+ logger.info(f"微信支付预下单成功,prepay_id={prepay_id}")
+
+ return {
+ 'appId': APPID,
+ 'timeStamp': timestamp,
+ 'nonceStr': nonce_str,
+ 'package': package,
+ 'signType': sign_type,
+ 'paySign': pay_sign,
+ }
+ else:
+ # 安全获取错误信息
+ err_code_node = root.find('err_code')
+ err_code = err_code_node.text if err_code_node is not None else '未知错误码'
+ err_code_des_node = root.find('err_code_des')
+ err_code_des = err_code_des_node.text if err_code_des_node is not None else '未知错误描述'
+ error_details = f"return_code: {return_code}, result_code: {result_code}, err_code: {err_code}, err_code_des: {err_code_des}"
+ logger.error(f"微信支付下单失败: {error_details}")
+ raise Exception(f"微信支付下单失败: {error_details}")
+
+ def get_client_ip(self):
+ """获取客户端IP"""
+ request = self.request
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ if x_forwarded_for:
+ ip = x_forwarded_for.split(',')[0]
+ else:
+ ip = request.META.get('REMOTE_ADDR', '127.0.0.1')
+ return ip if ip else '127.0.0.1'
+
+
+class KaohePayCallbackView(View):
+ """
+ 考核支付回调接口
+ 路径:/yonghu/kh_huitiao
+ """
+
+ def post(self, request):
+ try:
+ request_body = request.body
+ logger.info(f"收到考核支付回调: {request_body.decode('utf-8')}")
+
+ root = ET.fromstring(request_body)
+
+ # 安全获取return_code
+ return_code_node = root.find('return_code')
+ if return_code_node is None or return_code_node.text is None:
+ logger.error(f"微信回调XML缺少return_code节点,原始内容: {request_body.decode('utf-8')}")
+ return self.wechat_response('FAIL', '数据格式错误')
+ return_code = return_code_node.text
+
+ # 安全获取result_code
+ result_code_node = root.find('result_code')
+ if result_code_node is None or result_code_node.text is None:
+ logger.error(f"微信回调XML缺少result_code节点,原始内容: {request_body.decode('utf-8')}")
+ return self.wechat_response('FAIL', '数据格式错误')
+ result_code = result_code_node.text
+
+ # 安全获取out_trade_no
+ out_trade_no_node = root.find('out_trade_no')
+ if out_trade_no_node is None or out_trade_no_node.text is None:
+ logger.error(f"微信回调XML缺少out_trade_no节点,原始内容: {request_body.decode('utf-8')}")
+ return self.wechat_response('FAIL', '数据格式错误')
+ out_trade_no = out_trade_no_node.text
+
+ # 安全获取transaction_id
+ transaction_id_node = root.find('transaction_id')
+ if transaction_id_node is None or transaction_id_node.text is None:
+ logger.error(f"微信回调XML缺少transaction_id节点,原始内容: {request_body.decode('utf-8')}")
+ return self.wechat_response('FAIL', '数据格式错误')
+ transaction_id = transaction_id_node.text
+
+ # 安全获取total_fee
+ total_fee_node = root.find('total_fee')
+ if total_fee_node is None or total_fee_node.text is None:
+ logger.error(f"微信回调XML缺少total_fee节点,原始内容: {request_body.decode('utf-8')}")
+ return self.wechat_response('FAIL', '数据格式错误')
+ total_fee = int(total_fee_node.text)
+
+ logger.info(
+ f"回调参数解析成功: out_trade_no={out_trade_no}, transaction_id={transaction_id}, total_fee={total_fee}")
+
+ if return_code != 'SUCCESS' or result_code != 'SUCCESS':
+ logger.warning(f"支付状态异常: return_code={return_code}, result_code={result_code}")
+ return self.wechat_response('FAIL', '支付失败')
+
+ # 验证签名
+ if not self.verify_signature(request_body):
+ logger.error(f"签名验证失败,订单号: {out_trade_no}")
+ return self.wechat_response('FAIL', '签名验证失败')
+ logger.info(f"签名验证成功,订单号: {out_trade_no}")
+
+ # 查找订单
+ try:
+ order = Czjilu.objects.get(dingdan_id=out_trade_no, leixing=5)
+ logger.info(f"找到订单: {out_trade_no}, 当前状态: {order.zhuangtai}")
+ except Czjilu.DoesNotExist:
+ logger.error(f"考核订单不存在: {out_trade_no}")
+ return self.wechat_response('FAIL', '订单不存在')
+
+ if order.zhuangtai != 9:
+ logger.warning(f"订单状态非未支付: {order.zhuangtai}, 订单号: {out_trade_no}")
+ # 已支付的订单直接返回成功,避免微信重复回调
+ if order.zhuangtai == 3:
+ logger.info(f"订单已支付,直接返回成功: {out_trade_no}")
+ return self.wechat_response('SUCCESS', 'OK')
+ return self.wechat_response('FAIL', '订单状态异常')
+
+ # 验证金额
+ wechat_yuan = total_fee / 100
+ if abs(wechat_yuan - float(order.jine)) > 0.01:
+ logger.error(f"金额不一致: 订单金额={order.jine}, 回调金额={wechat_yuan}, 订单号: {out_trade_no}")
+ return self.wechat_response('FAIL', '金额不一致')
+ logger.info(f"金额验证通过: {wechat_yuan}元")
+
+ # 更新订单状态为已支付
+ order.zhuangtai = 3
+ order.save()
+ logger.info(f"订单状态更新为已支付: {out_trade_no}")
+
+ # 从临时表获取业务参数并创建审核记录
+ try:
+ temp = KaohePayTemp.objects.get(dingdan_id=out_trade_no, yonghuid=order.yonghuid)
+ logger.info(f"找到临时表记录: {out_trade_no}, 用户ID: {order.yonghuid}")
+
+ user = UserMain.objects.get(yonghuid=order.yonghuid)
+ logger.info(f"获取用户信息成功: {user.yonghuid}")
+
+ success, msg, record = create_shenhe_jilu_from_temp(user, temp)
+ if success:
+ logger.info(f"创建审核记录成功: jilu_id={record.jilu_id}, 订单号: {out_trade_no}")
+ else:
+ logger.error(f"创建审核记录失败: {msg}, 订单号: {out_trade_no}")
+ # 订单已支付但记录创建失败,需要标记订单异常,人工介入
+ order.zhuangtai = 10 # 10表示支付成功但业务处理失败
+ order.save()
+ logger.error(f"订单状态标记为异常(10): {out_trade_no}")
+
+ except KaohePayTemp.DoesNotExist:
+ logger.error(f"临时表无记录,订单号: {out_trade_no}, 用户ID: {order.yonghuid}")
+ # 尝试从attach解析(兼容旧数据)
+ attach_element = root.find('attach')
+ if attach_element is not None and attach_element.text:
+ try:
+ attach_data = json.loads(attach_element.text)
+ logger.info(f"从attach解析数据成功: {attach_data}")
+ # 这里可以保留旧逻辑,但建议只记录日志
+ except Exception as e:
+ logger.error(f"解析attach失败: {str(e)}")
+ except Exception as e:
+ logger.error(f"处理临时表或创建审核记录异常: {str(e)}", exc_info=True)
+ # 标记订单异常
+ order.zhuangtai = 10
+ order.save()
+ logger.error(f"订单状态标记为异常(10): {out_trade_no}")
+
+ # 更新收支记录
+ try:
+ jine_decimal = Decimal(str(order.jine))
+ update_daily_income(jine_decimal)
+ logger.info(f"更新每日收入成功: {jine_decimal}元")
+
+ szjilu, created = Szjilu.objects.get_or_create(
+ id=1,
+ defaults={
+ 'zongsy': Decimal('0.00'),
+ 'zongls': Decimal('0.00'),
+ 'zongzc': Decimal('0.00'),
+ 'jrzc': Decimal('0.00'),
+ 'jrls': Decimal('0.00'),
+ }
+ )
+ szjilu.zongsy += jine_decimal
+ szjilu.zongls += jine_decimal
+ szjilu.jrls += jine_decimal
+ szjilu.save()
+ logger.info(f"更新收支记录成功: zongsy={szjilu.zongsy}")
+ except Exception as e:
+ logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
+
+ logger.info(f"考核支付回调处理完成,订单号: {out_trade_no}")
+ return self.wechat_response('SUCCESS', 'OK')
+
+ except ET.ParseError as e:
+ logger.error(f"XML解析失败: {str(e)}, 原始数据: {request.body.decode('utf-8')}")
+ return self.wechat_response('FAIL', '数据格式错误')
+ except Exception as e:
+ logger.error(f"考核支付回调异常: {str(e)}", exc_info=True)
+ return self.wechat_response('FAIL', '处理异常')
+
+ def verify_signature(self, xml_data):
+ """验证微信支付签名"""
+ try:
+ root = ET.fromstring(xml_data)
+ sign_node = root.find('sign')
+ if sign_node is None or sign_node.text is None:
+ logger.error("微信回调XML缺少sign节点")
+ return False
+ sign = sign_node.text
+
+ params = {}
+ for child in root:
+ if child.tag != 'sign' and child.text is not None:
+ params[child.tag] = child.text
+
+ sorted_params = sorted(params.items(), key=lambda x: x[0])
+ sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
+ sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
+ calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
+
+ result = sign == calculated_sign
+ if not result:
+ logger.error(f"签名验证失败: 原签名={sign}, 计算签名={calculated_sign}")
+ return result
+ except Exception as e:
+ logger.error(f"签名验证异常: {str(e)}", exc_info=True)
+ return False
+
+ def wechat_response(self, return_code, return_msg):
+ """构造微信回调响应XML"""
+ xml = f"""
+
+
+"""
+ logger.info(f"返回微信响应: {xml}")
+ return HttpResponse(xml, content_type='application/xml')
+
+
+
+
+
+
+
+class KaohePayPollView(APIView):
+ """考核支付轮询"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ dingdanid = request.data.get('dingdanid')
+ if not dingdanid:
+ return Response({'code': 400, 'message': '订单ID不能为空'})
+ try:
+ order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5)
+ if order.zhuangtai == 3:
+ return Response({'code': 200, 'message': '支付成功'})
+ return Response({'code': 400, 'message': '订单尚未支付'})
+ except Czjilu.DoesNotExist:
+ return Response({'code': 400, 'message': '订单不存在'})
+
+
+class KaohePayFailView(APIView):
+ """考核支付失败处理"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ dingdanid = request.data.get('dingdanid')
+ if not dingdanid:
+ return Response({'code': 400, 'message': '订单ID不能为空'})
+ try:
+ order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5)
+ if order.zhuangtai == 9:
+ order.delete()
+ return Response({'code': 200, 'message': '订单已删除'})
+ return Response({'code': 400, 'message': '订单状态无法删除'})
+ except Czjilu.DoesNotExist:
+ return Response({'code': 400, 'message': '订单不存在'})
+
+
+
+
+
+
+
+
+class TixianAssetView(APIView):
+ """
+ 获取用户所有可提现资产及费率
+ 请求方式:POST
+ 认证:JWT Token
+ 返回:
+ {
+ "code": 200,
+ "msg": "success",
+ "data": {
+ "dashou_yue": "123.45",
+ "dashou_yajin": "500.00",
+ "shangjia_yue": "0.00",
+ "guanshi_fenyong": "67.89",
+ "zuzhang_fenyong": "0.00",
+ "kaoheguan_fenyong": "0.00",
+ "dashou_rate": 0.08,
+ "dashou_yajin_rate": 0.05,
+ "shangjia_rate": 0.06,
+ "guanshi_rate": 0.08,
+ "zuzhang_rate": 0.05,
+ "kaoheguan_rate": 0.07
+ }
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ user = request.user
+ # 初始化返回数据,默认全部为0
+ data = {
+ 'dashou_yue': '0.00',
+ 'dashou_yajin': '0.00',
+ 'shangjia_yue': '0.00',
+ 'guanshi_fenyong': '0.00',
+ 'zuzhang_fenyong': '0.00',
+ 'kaoheguan_fenyong': '0.00',
+ 'dashou_rate': 0.00,
+ 'dashou_yajin_rate': 0.00,
+ 'shangjia_rate': 0.00,
+ 'guanshi_rate': 0.00,
+ 'zuzhang_rate': 0.00,
+ 'kaoheguan_rate': 0.00,
+ }
+
+ # ------------------- 打手资产 -------------------
+ try:
+ dashou = user.dashou_profile
+ data['dashou_yue'] = str(dashou.yue)
+ data['dashou_yajin'] = str(dashou.yajin)
+ except ObjectDoesNotExist:
+ pass
+
+ # ------------------- 商家资产 -------------------
+ try:
+ shangjia = user.shop_profile
+ data['shangjia_yue'] = str(shangjia.yue)
+ except ObjectDoesNotExist:
+ pass
+
+ # ------------------- 管事资产 -------------------
+ try:
+ guanshi = user.guanshi_profile
+ data['guanshi_fenyong'] = str(guanshi.yue)
+ except ObjectDoesNotExist:
+ pass
+
+ # ------------------- 组长资产 -------------------
+ try:
+ zuzhang = user.zuzhang_profile
+ data['zuzhang_fenyong'] = str(zuzhang.ketixian_jine)
+ except ObjectDoesNotExist:
+ pass
+
+ # ------------------- 审核官(考核官)资产 -------------------
+ try:
+ kaoheguan = user.shenheguan_profile
+ data['kaoheguan_fenyong'] = str(kaoheguan.yue)
+ except ObjectDoesNotExist:
+ pass
+
+ # ------------------- 费率查询(从 Lilubiao 获取) -------------------
+ # 打手提现费率 (类型 '5')
+ lilu_obj = Lilubiao.objects.filter(fadanpingtai='5').first()
+ data['dashou_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
+
+ # 打手保证金提现费率 (类型 '11')
+ lilu_obj = Lilubiao.objects.filter(fadanpingtai='11').first()
+ data['dashou_yajin_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
+
+ # 商家提现费率 (类型 '10')
+ lilu_obj = Lilubiao.objects.filter(fadanpingtai='10').first()
+ data['shangjia_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
+
+ # 管事提现费率 (类型 '6')
+ lilu_obj = Lilubiao.objects.filter(fadanpingtai='6').first()
+ data['guanshi_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
+
+ # 组长提现费率 (类型 '8')
+ lilu_obj = Lilubiao.objects.filter(fadanpingtai='8').first()
+ data['zuzhang_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
+
+ # 审核官(考核官)提现费率 (类型 '9')
+ lilu_obj = Lilubiao.objects.filter(fadanpingtai='9').first()
+ data['kaoheguan_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
+
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': data
+ }, status=status.HTTP_200_OK)
+
+
+
+
+
+class GuanshiContactView(APIView):
+ """
+ 管事获取自己的邀请人(组长)信息
+ 路径: /api/yonghu/guanshi_contact
+ 方法: POST
+ 权限: JWT认证
+ 请求体: 无(仅需携带合法token)
+ 成功返回:
+ code: 200,
+ data: {
+ uid: "1234567", // 组长用户ID
+ nicheng: "张三", // 组长昵称
+ touxiang: "avatar/xxx.jpg" // 组长头像相对路径(前端拼接完整URL)
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 获取当前登录用户(必须已通过JWT认证)
+ current_user = request.user # 类型: UserMain
+
+ # 2. 校验是否为管事身份(存在管事扩展表记录)
+ try:
+ guanshi_profile = current_user.guanshi_profile
+ except ObjectDoesNotExist:
+ return Response({
+ 'code': 403,
+ 'msg': '您还不是管事,无法获取组长信息',
+ 'data': None
+ })
+
+ # 3. 获取邀请人ID(管事扩展表中的 yaoqingren 字段存储的是邀请人的 yonghuid)
+ inviter_yonghuid = guanshi_profile.yaoqingren
+ if not inviter_yonghuid:
+ return Response({
+ 'code': 404,
+ 'msg': '未找到邀请人信息',
+ 'data': None
+ })
+
+ # 4. 根据邀请人用户ID查询用户主表
+ try:
+ inviter_user = UserMain.objects.get(yonghuid=inviter_yonghuid)
+ except UserMain.DoesNotExist:
+ logger.error(f"邀请人用户不存在: yonghuid={inviter_yonghuid}")
+ return Response({
+ 'code': 404,
+ 'msg': '邀请人账号异常',
+ 'data': None
+ })
+
+ # 5. 获取邀请人昵称(从老板扩展表取,若不存在则给默认值)
+ try:
+ inviter_nicheng = inviter_user.boss_profile.nickname
+ except ObjectDoesNotExist:
+ inviter_nicheng = '组长' # 兜底昵称
+
+ # 6. 构造返回数据
+ response_data = {
+ 'uid': inviter_user.yonghuid,
+ 'nicheng': inviter_nicheng,
+ 'touxiang': inviter_user.avatar or '' # 头像相对路径
+ }
+
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': response_data
+ })
+
+
+
+
+
+
+
+
+
+logger = logging.getLogger(__name__) # 恢复区间?
+
+def generate_tixian_id():
+ """生成唯一提现订单号:TX + 时间戳(13位) + 4位随机数"""
+ timestamp = str(int(time.time() * 1000))
+ rand = ''.join(random.choices(string.digits, k=4))
+ return f'TX{timestamp}{rand}'
+
+
+class TixianShenqingV3View(APIView):
+ """
+ 自动提现收款接口(新流程第二步)
+ POST /yonghu/tixiansq
+ 必须传 shenhe_danhao;审核状态须为 6=待收款;申请时已扣款,本接口不再扣款
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ shenhe_danhao = (request.data.get('shenhe_danhao') or '').strip()
+ if shenhe_danhao:
+ return process_audit_collect(request)
+ return Response({
+ 'code': 7,
+ 'msg': '缺少审核单号,请先提交提现申请(/yonghu/zddksh)并等待审核通过后再收款',
+ })
+
+
+
+class TixianCallbackV3View(APIView):
+ """
+ 微信转账结果回调接口(最终状态通知)
+ 处理微信异步发送的转账成功/失败结果
+ """
+ permission_classes = [] # 无需认证,签名验证保证安全
+
+ def post(self, request):
+ headers = request.headers
+ body = request.body.decode('utf-8')
+
+ # 1. 验证签名
+ if not verify_wechat_sign(headers, body):
+ return Response({'code': 'FAIL', 'message': '签名验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 解密数据
+ try:
+ data = json.loads(body)
+ resource = data.get('resource')
+ if not resource:
+ return Response({'code': 'FAIL', 'message': 'resource不存在'}, status=status.HTTP_400_BAD_REQUEST)
+
+ associated_data = resource.get('associated_data', '')
+ nonce = resource.get('nonce')
+ ciphertext = resource.get('ciphertext')
+ if not nonce or not ciphertext:
+ return Response({'code': 'FAIL', 'message': '解密字段缺失'}, status=status.HTTP_400_BAD_REQUEST)
+
+ plaintext = decrypt_callback_ciphertext(associated_data, nonce, ciphertext)
+ callback_data = json.loads(plaintext)
+ except Exception as e:
+ logger.error(f"回调解密失败: {str(e)}", exc_info=True)
+ return Response({'code': 'FAIL', 'message': f'解密失败: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 获取关键信息
+ out_bill_no = callback_data.get('out_bill_no') # 商户提现单号(即 tixian_id)
+ transfer_status = callback_data.get('transfer_status') # SUCCESS / FAIL
+ wechat_transfer_no = callback_data.get('transfer_no') # 微信转账单号
+ fail_reason = callback_data.get('fail_reason', '')
+
+ if not out_bill_no:
+ return Response({'code': 'FAIL', 'message': 'out_bill_no缺失'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 幂等处理:只更新状态为0(处理中)的记录,已最终状态不再处理
+ with transaction.atomic():
+ try:
+ auto_record = TixianAutoRecord.objects.select_for_update().get(tixian_id=out_bill_no)
+ except TixianAutoRecord.DoesNotExist:
+ # 记录不存在,视为已处理,返回成功
+ return Response({'code': 'SUCCESS', 'message': 'ok'})
+
+ # 如果已经是最新状态,直接返回成功(幂等)
+ if auto_record.zhuangtai != 0:
+ return Response({'code': 'SUCCESS', 'message': 'already processed'})
+
+ if transfer_status == 'SUCCESS':
+ mark_transfer_success(
+ auto_record,
+ wechat_transfer_no=wechat_transfer_no,
+ with_accounting=True,
+ )
+
+ else: # transfer_status == 'FAIL'
+ auto_record.zhuangtai = 2
+ auto_record.fail_reason = fail_reason or '微信转账失败'
+ auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
+
+ if not auto_record.shenhe_danhao:
+ logger.error('回调失败但打款记录无审核单号 bill=%s', out_bill_no)
+ else:
+ # 申请时已扣款,回调失败不退余额,恢复待收款(6)可重试
+ try:
+ audit = TixianShenheJilu.objects.select_for_update().get(
+ shenhe_danhao=auto_record.shenhe_danhao
+ )
+ audit.tixian_auto_id = None
+ audit.fail_reason = auto_record.fail_reason
+ sync_audit_and_jilu_status(audit, 6, fail_reason=auto_record.fail_reason)
+ except TixianShenheJilu.DoesNotExist:
+ logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}')
+ release_collect_quota_for_record(auto_record)
+
+ return Response({'code': 'SUCCESS', 'message': 'ok'})
+
+
+
+class TixianQueRenAutoView(APIView):
+ """
+ 用户确认提现结果
+ POST /yonghu/tixianqr
+ 新审核流程:打款记录带 shenhe_danhao 时,确认成功同步审核表+提现记录表为 2;取消不退款(申请已扣),恢复待收款 6 可重试
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ try:
+ tixian_id = request.data.get('tixian_id')
+ result = request.data.get('result') # 1成功 0失败/取消
+
+ if not tixian_id or result is None:
+ return Response({'code': 1, 'msg': '缺少参数'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ result = int(result)
+ except ValueError:
+ return Response({'code': 2, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if result not in [0, 1]:
+ return Response({'code': 3, 'msg': 'result值无效'}, status=status.HTTP_400_BAD_REQUEST)
+
+ user_main = request.user
+
+ with transaction.atomic():
+ try:
+ auto_record = TixianAutoRecord.objects.select_for_update().get(
+ tixian_id=tixian_id,
+ yonghuid=user_main.yonghuid,
+ )
+ except TixianAutoRecord.DoesNotExist:
+ return Response({'code': 4, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ if result == 1:
+ if auto_record.zhuangtai == 1:
+ return Response({'code': 0, 'msg': '提现成功', 'data': None})
+
+ if auto_record.zhuangtai != 0:
+ return Response({'code': 5, 'msg': '该提现已处理'})
+
+ mark_transfer_success(auto_record, with_accounting=True)
+
+ return Response({'code': 0, 'msg': '提现成功', 'data': None})
+
+ # 用户取消或失败
+ auto_record.zhuangtai = 2
+ auto_record.fail_reason = '用户取消收款'
+ auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
+
+ if not auto_record.shenhe_danhao:
+ logger.error('确认取消但打款记录无审核单号 bill=%s', tixian_id)
+ return Response({'code': 5, 'msg': '该提现已处理'})
+
+ # 申请时已扣款,取消不退余额,审核单恢复 6 待收款,可再次点收款
+ try:
+ audit = TixianShenheJilu.objects.select_for_update().get(
+ shenhe_danhao=auto_record.shenhe_danhao,
+ )
+ audit.tixian_auto_id = None
+ sync_audit_and_jilu_status(
+ audit, 6,
+ fail_reason='用户取消收款,可重新发起',
+ )
+ except TixianShenheJilu.DoesNotExist:
+ logger.warning(f'确认取消但审核记录不存在: {auto_record.shenhe_danhao}')
+ release_collect_quota_for_record(auto_record)
+
+ return Response({'code': 0, 'msg': '提现已取消', 'data': None})
+
+ except Exception as e:
+ logger.error(f'提现确认异常: {str(e)}', exc_info=True)
+ return Response({'code': 99, 'msg': '系统错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
diff --git a/utils/chat_utils.py b/utils/chat_utils.py
index d6e6bed..b33287b 100644
--- a/utils/chat_utils.py
+++ b/utils/chat_utils.py
@@ -3,9 +3,9 @@ import json
import logging
import requests
from django.conf import settings
-from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia
-from peizhi.models import ClubConfig
-from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia
+from orders.models import Dingdan, DingdanPingtai, DingdanShangjia
+from config.models import ClubConfig
+from users.models import UserMain, UserDashou, UserBoss, UserShangjia
logger = logging.getLogger('chat_utils')
diff --git a/utils/fadan_utils.py b/utils/fadan_utils.py
index fdfda71..8abd86f 100644
--- a/utils/fadan_utils.py
+++ b/utils/fadan_utils.py
@@ -1,5 +1,5 @@
# utils/fadan_utils.py
-from dingdan.models import Fadan
+from orders.models import Fadan
import logging
logger = logging.getLogger('xiaochengxu')
diff --git a/utils/invitationcode_utils.py b/utils/invitationcode_utils.py
new file mode 100644
index 0000000..55a2b6c
--- /dev/null
+++ b/utils/invitationcode_utils.py
@@ -0,0 +1,56 @@
+"""
+阿龙电竞陪玩平台 - 邀请码生成工具
+生成规则:毫秒时间戳 + 用户ID + 随机数 -> Base62编码 -> 固定20位邀请码
+特点:唯一性高、生成快、无序不可读
+"""
+import time
+import random
+
+# Base62 编码字符集 (数字+小写字母+大写字母)
+_BASE62_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+_BASE62_LENGTH = len(_BASE62_CHARS)
+
+
+def base62_encode(num: int) -> str:
+ """
+ 将整数转换为 Base62 字符串,固定长度为 20 位
+ """
+ if not num:
+ return _BASE62_CHARS[0]
+ res = []
+ while num:
+ num, r = divmod(num, _BASE62_LENGTH)
+ res.append(_BASE62_CHARS[r])
+ return ''.join(reversed(res))
+
+
+def CreateInvitationCode(uid_str: str) -> str:
+ """
+ 创建唯一邀请码 (20位固定长度)
+
+ 参数:
+ uid_str: 用户ID字符串,如 "123456"
+
+ 返回:
+ str: 生成的唯一邀请码
+ """
+ ts = int(time.time()*1000)
+ try:
+ uid = int(uid_str)
+ except ValueError:
+ uid = abs(hash(uid_str)) % 10**8
+ num = int(f"{ts}{uid:08d}{random.randint(0,9999):04d}")
+ res = base62_encode(num)
+ return res.rjust(20, _BASE62_CHARS[0]) if len(res)<20 else res[-20:]
+
+
+def VerifyInvitationCode(yaoqingma: str) -> bool:
+ """
+ 验证邀请码是否有效 (简单的格式验证)
+ 参数:
+ yaoqingma: 待验证的邀请码
+ 返回:
+ bool: 邀请码格式是否有效
+ """
+
+ return bool(yaoqingma and len(yaoqingma)==20 and all(c in _BASE62_CHARS for c in yaoqingma))
\ No newline at end of file
diff --git a/utils/weixin_broadcast.py b/utils/weixin_broadcast.py
index 47774b6..88c1b4e 100644
--- a/utils/weixin_broadcast.py
+++ b/utils/weixin_broadcast.py
@@ -5,7 +5,7 @@ import json # 新增,用于格式化打印
import requests
from django.core.cache import cache
from django.conf import settings
-from yonghu.models import OfficialAccountUser
+from users.models import OfficialAccountUser
logger = logging.getLogger('weixin_broadcast')
diff --git a/utils/yaoqingma_utils.py b/utils/yaoqingma_utils.py
deleted file mode 100644
index 6ddcfbc..0000000
--- a/utils/yaoqingma_utils.py
+++ /dev/null
@@ -1,89 +0,0 @@
-"""
-阿龙电竞陪玩平台 - 邀请码生成工具
-生成规则:毫秒时间戳 + 用户ID + 随机数 -> Base62编码 -> 固定20位邀请码
-特点:唯一性高、生成快、无序不可读
-"""
-import time
-import random
-
-# Base62 编码字符集 (数字+小写字母+大写字母)
-_BASE62_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
-_BASE62_LENGTH = 62
-
-
-def base62_encode(num):
- """将数字转换为Base62字符串"""
- if num == 0:
- return _BASE62_CHARS[0]
-
- result = []
- while num > 0:
- num, remainder = divmod(num, _BASE62_LENGTH)
- result.append(_BASE62_CHARS[remainder])
-
- # 反转结果字符串
- return ''.join(reversed(result))
-
-
-def chuangjianYaoqingma(yonghuid_str):
- """
- 创建唯一邀请码 (20位固定长度)
-
- 参数:
- yonghuid_str: 用户ID字符串,如 "123456"
-
- 返回:
- 20位的Base62编码邀请码字符串
- """
- # 1. 获取当前毫秒时间戳 (13位)
- timestamp_ms = int(time.time() * 1000)
-
- # 2. 将用户ID字符串转换为数字 (确保处理字符串)
- try:
- # 先尝试直接转换整数
- yonghuid_num = int(yonghuid_str)
- except ValueError:
- # 如果用户ID不是纯数字,使用哈希值
- yonghuid_num = abs(hash(yonghuid_str)) % (10 ** 8) # 取8位数字
-
- # 3. 生成一个随机数 (0-9999)
- random_salt = random.randint(0, 9999)
-
- # 4. 组合数字: 时间戳(13位) + 用户ID数字(最多8位) + 随机数(4位)
- # 示例: 1647850123456 + 123456 + 7890 = 16478501234561234567890
- combined_num = int(f"{timestamp_ms}{yonghuid_num:08d}{random_salt:04d}")
-
- # 5. Base62编码
- encoded_str = base62_encode(combined_num)
-
- # 6. 确保固定20位长度: 不足补0,超过取后20位
- # 理论上组合数字非常大,编码后通常会超过20位
- if len(encoded_str) < 20:
- # 左补0 (理论上不会发生,但保持安全)
- encoded_str = encoded_str.rjust(20, _BASE62_CHARS[0])
- else:
- # 取后20位 (更随机)
- encoded_str = encoded_str[-20:]
-
- return encoded_str
-
-
-def yanzhengYaoqingmaWeiyixing(yaoqingma):
- """
- 验证邀请码是否有效 (简单的格式验证)
-
- 参数:
- yaoqingma: 待验证的邀请码
-
- 返回:
- bool: 邀请码格式是否有效
- """
- if not yaoqingma or len(yaoqingma) != 20:
- return False
-
- # 检查是否只包含Base62字符
- for char in yaoqingma:
- if char not in _BASE62_CHARS:
- return False
-
- return True
\ No newline at end of file