diff --git a/.gitignore b/.gitignore index 4524e33..af3e880 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ db.sqlite3 /venv/ /env/ .env -_secrets.py +app_secrets.py staticfiles/ media/ *.log diff --git a/_secrets.py b/_secrets.py deleted file mode 100644 index 51f2992..0000000 --- a/_secrets.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -阿龙电竞 - 密钥与敏感配置文件 -此文件包含所有敏感信息,不应提交到版本控制。 -部署时请根据实际环境修改对应值。 -""" - -# ==================== Django 核心 ==================== -SECRET_KEY = 'django-insecure-9+xu)m%#!#qblgby5sq!sjyg5nj9lir78+&*f$28$459#yktej' - -# ==================== 数据库 ==================== -DATABASE_NAME = 'xaio_cheng_xu' -DATABASE_USER = 'root' -DATABASE_PASSWORD = 'sajksh.sdfGH3YUge.wjkd+' -DATABASE_HOST = '172.17.0.15' -DATABASE_PORT = '3306' - -# ==================== Redis ==================== -REDIS_HOST = '172.19.0.2' -REDIS_PORT = 6379 -REDIS_PASSWORD = 'Dujieduze5.3' -REDIS_DB_CELERY = 0 -REDIS_DB_RESULT = 1 - -# ==================== 腾讯云 COS ==================== -COS_SECRET_ID = 'AKIDU6WikZBcz96xesxpVM3XoslS3TjogLDx' -COS_SECRET_KEY = 'qcJzVqLdkeKyXwKckjV5SCmncFfrqdu2' -COS_REGION = 'ap-shanghai' -COS_BUCKET = 'xingque999qygwuyq-1404472910' -COS_APPID = '1404472910' - -# ==================== 微信小程序 ==================== -WEIXIN_APPID = 'wx0e4be86faac4a8d1' -WEIXIN_SECRET = '3c3673cf052571dea9e3bea88e198434' - -# ==================== 微信支付 V2 ==================== -WEIXIN_MCHID = '1746545364' -WEIXIN_SHANGHUMIYAO = 'Dujieduze53631383771411017633490' -WEIXIN_CERT_PATH = '/opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.1/tuikuanzhengshu/apiclient_cert.pem' -WEIXIN_KEY_PATH = '/opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.1/tuikuanzhengshu/apiclient_key.pem' - -# ==================== 微信服务号 ==================== -WEIXIN_OFFICIAL_APPID = 'wxd400adf1911c534f' -WEIXIN_OFFICIAL_SECRET = '19c946cf02ac6ef13f40333fc8029b92' -WEIXIN_OFFICIAL_TOKEN = '78324hjwewehrui3yferhf34' -WEIXIN_OFFICIAL_ENCODING_AES_KEY = 'NimrG1Fmq2ydplHJrXeg34NqbCJerltAGVx9xJ5WBx2' -WEIXIN_TEMPLATE_ID = 'jZ73G_0KbmFK864WVUr0QFNnqdH-ILBp_hUPMiwx4YY' - -# ==================== 微信支付 V3 ==================== -WECHAT_PAY_V3_MCHID = '1746545364' -WECHAT_PAY_V3_APPID = 'wx0e4be86faac4a8d1' -WECHAT_PAY_V3_API_V3_KEY = 'dujie4637rufjd98ijuyrgcvbvbn4789' -WECHAT_PAY_V3_PRIVATE_KEY_PATH = '/opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.443/huizhifuzhengshu/apiclient_key.pem' -WECHAT_PAY_V3_CERT_SERIAL_NO = '13D0CECB42C48DE78FF35A8A72EF4341FE769FE9' -WECHAT_PAY_V3_PLATFORM_CERT_DIR = '/opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.443/huizhifuzhengshu/pub_key.pem' - -# ==================== GoEasy IM ==================== -GOEASY_APPKEY = 'BC-32e2991c69bc47e1b538e73aaed37fc1' -GOEASY_SECRET = 'A469ABAA3cc34D64' - -# ==================== 跨平台互通 ==================== -CROSS_PLATFORM_TOKEN = 'jdjdjshwjqk' -CROSS_PLATFORM_SYNC_TOKEN = 'xK9mP2vL8qW5rT7y' -PARTNER_BASE_URL = 'https://ios808.top/hqhd' -AGENT_SHANGJIA_ID = '9106027' -CROSS_PLATFORM_MEMBER_ID = 'V00QQI' -CROSS_PLATFORM_WHITELIST = ['5438898'] - -# ==================== 特殊管事ID ==================== -SPECIAL_GUANSHI_IDS = [ - '2295516', - '9106027', - '2244062', -] diff --git a/a_long_dianjing/settings.py b/a_long_dianjing/settings.py index 3e86917..60bb888 100644 --- a/a_long_dianjing/settings.py +++ b/a_long_dianjing/settings.py @@ -1,6 +1,5 @@ """ Django settings for a_long_dianjing project. -开发环境配置 - 参照旧项目结构精简 """ from pathlib import Path from datetime import timedelta @@ -8,19 +7,17 @@ import os # 从密钥配置文件导入敏感信息 try: - import _secrets + import app_secrets except ImportError: - raise ImportError("缺少 secrets.py 密钥配置文件,请参照项目说明创建") + raise ImportError("缺少 appapp_secrets.py 密钥配置文件,请参照项目说明创建") -# Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -# Quick-start development settings - unsuitable for production -SECRET_KEY = _secrets.SECRET_KEY -DEBUG = False # 开发环境为 True -ALLOWED_HOSTS = ['localhost', '127.0.0.1','www.abas.asia'] # 根据开发前端地址可添加 +SECRET_KEY = app_secrets.SECRET_KEY +DEBUG = False +ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'www.abas.asia'] -# Application definition +# ==================== 应用配置 ==================== INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', @@ -28,34 +25,30 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - # 第三方应用 (根据 requirements.txt 已安装的) 'rest_framework', 'rest_framework_simplejwt', 'corsheaders', 'django_celery_results', - # 你的自定义应用 (按已创建的拼音命名) 'celery', 'yonghu', 'dingdan', 'shangpin', 'peizhi', 'houtai', - # 'games', # 【待确认】你提到 games 还没创建,暂不添加 'utils', 'shangdian', 'dengji', ] MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', # 跨域中间件放最前 + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', # 默认开启,如API无需可注释 + 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - # 'guanapi.middleware.OpLogMiddleware', # 【待确认】这是旧项目的自定义中间件,新项目需要吗? ] ROOT_URLCONF = 'a_long_dianjing.urls' @@ -77,57 +70,40 @@ TEMPLATES = [ WSGI_APPLICATION = 'a_long_dianjing.wsgi.application' -# Database -# 根据你提供的信息配置 (本地开发,端口3307) +# ==================== 数据库 ==================== DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', - 'NAME': _secrets.DATABASE_NAME, - 'USER': _secrets.DATABASE_USER, - 'HOST': _secrets.DATABASE_HOST, - 'PASSWORD': _secrets.DATABASE_PASSWORD, - 'PORT': _secrets.DATABASE_PORT, + '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'", # 保持与旧项目一致 + 'init_command': "SET time_zone='+08:00'", } } } -# Password validation +# ==================== 密码验证 ==================== 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', - }, + {'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'}, ] -# Internationalization -LANGUAGE_CODE = 'zh-Hans' # 使用中文 +# ==================== 国际化 ==================== +LANGUAGE_CODE = 'zh-Hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True -USE_TZ = False # 保持与旧项目一致,不使用UTC时区 +USE_TZ = False -# Static files (CSS, JavaScript, Images) STATIC_URL = 'static/' -# 开发环境静态文件目录,可参照旧项目添加 MEDIA 配置 -# MEDIA_URL = 'media/' -# MEDIA_ROOT = BASE_DIR / 'media' - -# Default primary key field type DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -# ==================== 以下为第三方库及自定义配置 ==================== -# settings.py -# 修改SIMPLE_JWT配置 +# ==================== JWT 配置 ==================== SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(days=36500), 'REFRESH_TOKEN_LIFETIME': timedelta(days=36500), @@ -143,7 +119,6 @@ SIMPLE_JWT = { 'LEEWAY': 0, 'AUTH_HEADER_TYPES': ('Bearer',), 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', - # 修改这里:使用yonghuid而不是id 'USER_ID_FIELD': 'yonghuid', 'USER_ID_CLAIM': 'yonghuid', 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', @@ -153,16 +128,13 @@ SIMPLE_JWT = { 'JTI_CLAIM': 'jti', } -# 添加自定义认证后端 AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend', ] -# 必须指定用户模型(即使它不继承AbstractBaseUser) AUTH_USER_MODEL = 'yonghu.UserMain' - -# DRF配置(也非常简单) +# ==================== DRF 配置 ==================== REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', @@ -171,48 +143,27 @@ REST_FRAMEWORK = { 'rest_framework.permissions.IsAuthenticated', ), 'DEFAULT_THROTTLE_CLASSES': [ - 'rest_framework.throttling.AnonRateThrottle', # 匿名用户限制 - 'rest_framework.throttling.UserRateThrottle', # 登录用户限制 + 'rest_framework.throttling.AnonRateThrottle', + 'rest_framework.throttling.UserRateThrottle', ], 'DEFAULT_THROTTLE_RATES': { - 'anon': '100/minute', # 匿名用户每分钟100次 - 'user': '1000/minute', # 登录用户每分钟1000次 + 'anon': '100/minute', + 'user': '1000/minute', }, - # 🔥 关键:使用自定义异常处理器 'EXCEPTION_HANDLER': 'utils.exception_handler.custom_exception_handler', - - # 🔥 生产环境只返回JSON,不返回HTML(更安全) 'DEFAULT_RENDERER_CLASSES': [ 'rest_framework.renderers.JSONRenderer', ], } +# ==================== H5 域名 ==================== +H5_DOMAIN = 'https://www.abas.asia:666' - - - - - - -# settings.py -# H5静态页面域名配置 -H5_DOMAIN = 'https://www.abas.asia:666' # 生产环境 -# H5_DOMAIN = 'https://test-h5.yourdomain.com' # 测试环境 - - - - - - - - - -# CORS settings +# ==================== CORS 配置 ==================== CORS_ALLOWED_ORIGINS = [ - "http://localhost:8080", # 假设前端开发服务器地址 + "http://localhost:8080", "http://127.0.0.1:8080", "https://www.abas.asia", - #"http://192.168.240.1:5173/", "https://www.abas.asia:666", "https://www.abas.asia:1234", "https://www.abas.asia:963", @@ -220,126 +171,73 @@ CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", "http://localhost:3000", "http://localhost:3001", - - - #"www.abas.asia", ] CORS_ALLOW_CREDENTIALS = True -CORS_ALLOW_METHODS = ( - "GET", - "POST", - "PUT", - "PATCH", - "DELETE", - "OPTIONS", -) +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", + "accept", "accept-encoding", "authorization", "content-type", + "dnt", "origin", "user-agent", "x-csrftoken", "x-requested-with", ] -# 【待确认】自定义用户模型,你需要在 yonghu/models.py 中创建 class Yonghu -#AUTH_USER_MODEL = 'yonghu.Yonghu' - - - - - - -# OSS配置 -COS_SECRET_ID = _secrets.COS_SECRET_ID -COS_SECRET_KEY = _secrets.COS_SECRET_KEY -COS_REGION = _secrets.COS_REGION -COS_BUCKET = _secrets.COS_BUCKET -COS_SCHEME = 'https' # 使用HTTPS +# ==================== 腾讯云 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 = _secrets.COS_APPID -COS_STS_DURATION = 7200 # STS临时凭证有效期(秒) +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_APPID = _secrets.WEIXIN_APPID -WEIXIN_SECRET = _secrets.WEIXIN_SECRET -WEIXIN_MCHID = _secrets.WEIXIN_MCHID -WEIXIN_SHANGHUMIYAO = _secrets.WEIXIN_SHANGHUMIYAO -# 服务号配置 -WEIXIN_OFFICIAL_APPID = _secrets.WEIXIN_OFFICIAL_APPID -WEIXIN_OFFICIAL_SECRET = _secrets.WEIXIN_OFFICIAL_SECRET -WEIXIN_OFFICIAL_TOKEN = _secrets.WEIXIN_OFFICIAL_TOKEN -WEIXIN_OFFICIAL_ENCODING_AES_KEY = _secrets.WEIXIN_OFFICIAL_ENCODING_AES_KEY -WEIXIN_TEMPLATE_ID = _secrets.WEIXIN_TEMPLATE_ID - -# 🆕【新增】模板消息配置 -WEIXIN_TEMPLATE_MAX_PER_MINUTE = 950 # 每分钟最大发送量(留50缓冲) -WEIXIN_TEMPLATE_BATCH_SIZE = 100 # 每批发送数量 - -WEIXIN_CERT_PATH = _secrets.WEIXIN_CERT_PATH -WEIXIN_KEY_PATH = _secrets.WEIXIN_KEY_PATH - -WEIXIN_NOTIFY_URL = 'https://www.abas.asia/hqhd/dingdan/pay-notify/' # 支付回调地址,如:http://你的域名/api/pay/callback/ -# settings.py 或相应配置文件中 -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" # 会员支付回调地址 - - -# 考核支付配置 +# ==================== 微信支付回调地址 ==================== +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 # 未配置费用时的默认金额 - - -# settings.py -SHANGJIA_CZ_PREFIX = "CzSJ" # 商家充值订单前缀 +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' - - -# 罚款缴纳微信支付配置 -FAKUAN_PAY_DESCRIPTION = '罚款缴纳' # 支付描述 -FAKUAN_PAY_NOTIFY_URL = 'https://www.abas.asia/hqhd/yonghu/fk_huitiao' # 微信支付回调地址(根据实际域名调整) - - - - -# 订单ID前缀配置 +# ==================== 订单ID前缀 ==================== ORDER_PREFIX = { - 'yajin': 'CzYJ', # 押金订单前缀 - 'jifen': 'CzJF', # 积分订单前缀 - 'huiyuan': 'CzHY', # 会员订单前缀 + 'yajin': 'CzYJ', + 'jifen': 'CzJF', + 'huiyuan': 'CzHY', } - - - - -# 腾讯云即时通讯 (IM) - 开发环境可暂不配置 +# ==================== 腾讯云 IM ==================== TENCENT_IM_CONFIG = { - 'SDK_APP_ID': 0, # 整数 + 'SDK_APP_ID': 0, 'SECRET_KEY': '', } -# 阿里云短信 - 开发环境可暂不配置 +# ==================== 阿里云短信 ==================== ALIYUN_SMS_CONFIG = { 'ACCESS_KEY_ID': '', 'ACCESS_KEY_SECRET': '', @@ -348,59 +246,27 @@ ALIYUN_SMS_CONFIG = { 'REGION_ID': 'cn-hangzhou', } -# HashID (用于加密ID,旧项目有,新项目需要吗?) -# HASHID_FIELD_SALT = 'a_long_dianjing_2025_salt' # 【待确认】请设置你的盐值 - - - # settings.py 尾部添加 -# settings.py -# settings.py -# settings.py 中添加配置 -GOEASY_APPKEY = _secrets.GOEASY_APPKEY -GOEASY_SECRET = _secrets.GOEASY_SECRET -GOEASY_REST_URL = 'https://rest-hz.goeasy.io/v2/im/message' # 正式环境 - - - - - - - - - - - -""" -阿龙电竞 - Django设置文件中的Celery配置部分 -请在您的settings.py中添加以下配置 -""" - -# ==================== Celery配置 ==================== -# Redis作为消息代理和结果后端 -REDIS_HOST = _secrets.REDIS_HOST -REDIS_PORT = _secrets.REDIS_PORT -REDIS_PASSWORD = _secrets.REDIS_PASSWORD -REDIS_DB_CELERY = _secrets.REDIS_DB_CELERY -REDIS_DB_RESULT = _secrets.REDIS_DB_RESULT - +# ==================== 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': { - # 不需要 CLIENT_CLASS - } + 'OPTIONS': {} } } - - - - -# 构建Redis连接字符串 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}' @@ -408,120 +274,56 @@ 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 - - - -# settings.py 末尾添加(在其他 Celery 配置之后) CELERY_IMPORTS = ('dingdan.tongzhi_tasks',) - - - -# 任务超时设置 -CELERY_TASK_TIME_LIMIT = 300 # 任务最大执行时间300秒 -CELERY_TASK_SOFT_TIME_LIMIT = 240 # 软超时240秒 - -# Worker并发设置 -CELERY_WORKER_CONCURRENCY = 4 # 根据服务器CPU核心数调整 +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', - }, + '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 -# 订单超时时间(秒) - 48小时 -ORDER_EXPIRE_SECONDS = 48000 * 60 * 60 # 172800秒 - -# 每日任务执行时间(24小时制) -DAILY_TASK_HOUR = 0 # 0点 -DAILY_TASK_MINUTE = 30 # 0分 - -# 每月任务执行时间 -MONTHLY_TASK_DAY = 1 # 每月1日 -MONTHLY_TASK_HOUR = 0 # 0点 -MONTHLY_TASK_MINUTE = 0 # 0分 - -# 订单检查频率(分钟) -ORDER_CHECK_INTERVAL = 5 # 每5分钟检查一次 - - - - - - - - - - - - -# ==================== 跨平台订单互通配置 ==================== -CROSS_PLATFORM_TOKEN = _secrets.CROSS_PLATFORM_TOKEN -PARTNER_BASE_URL = _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 = _secrets.AGENT_SHANGJIA_ID -CROSS_PLATFORM_MEMBER_ID = _secrets.CROSS_PLATFORM_MEMBER_ID - -# 默认游戏类型ID(当对方未传递时使用) +# ==================== 跨平台订单互通 ==================== +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 -CROSS_PLATFORM_WHITELIST = _secrets.CROSS_PLATFORM_WHITELIST - - - - - -SPECIAL_GUANSHI_IDS = _secrets.SPECIAL_GUANSHI_IDS - - - - - - - +# ==================== 微信支付 V3 ==================== WECHAT_PAY_V3_CONFIG = { - 'MCHID': _secrets.WECHAT_PAY_V3_MCHID, - 'APPID': _secrets.WECHAT_PAY_V3_APPID, - 'API_V3_KEY': _secrets.WECHAT_PAY_V3_API_V3_KEY, - 'PRIVATE_KEY_PATH': _secrets.WECHAT_PAY_V3_PRIVATE_KEY_PATH, - 'CERT_SERIAL_NO': _secrets.WECHAT_PAY_V3_CERT_SERIAL_NO, - 'PLATFORM_CERT_DIR': _secrets.WECHAT_PAY_V3_PLATFORM_CERT_DIR, + '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', } - - -CROSS_PLATFORM_SYNC_TOKEN = _secrets.CROSS_PLATFORM_SYNC_TOKEN \ No newline at end of file diff --git a/dingdan/views.py b/dingdan/views.py index bbe3a4a..3dee938 100644 --- a/dingdan/views.py +++ b/dingdan/views.py @@ -1,11494 +1,8146 @@ -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 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, DingdanShangjia, Chufajilu, Liaotian, Chufatupian -from utils.weixin_broadcast import WeixinBroadcastSender -from yonghu.models import UserMain, UserDashou, UserShangjia - -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 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 - -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): - """ - 前端支付失败接口 - 处理用户主动取消或支付失败 - 只允许删除状态为9(未支付)的订单 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}) - - with transaction.atomic(): - # 锁定订单防止并发 - dingdan = Dingdan.objects.select_for_update().filter(dingdan_id=dingdanid).first() - if not dingdan: - return Response({'code': 2, 'msg': '订单不存在', 'data': None}) - - # 只允许删除未支付订单(状态9) - if dingdan.zhuangtai != 9: - return Response({'code': 3, 'msg': '只能删除未支付的订单', 'data': None}) - - # 删除扩展表和主表 - DingdanPingtai.objects.filter(dingdan=dingdan).delete() - dingdan.delete() - - return Response({'code': 0, 'msg': '订单已删除', 'data': None}) - - except Exception as e: - 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): - """微信支付回调接口 - 接收微信官方支付结果通知(含详细调试日志)""" - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - - def post(self, request): - # 记录原始请求数据(调试用) - xml_data = request.body.decode('utf-8') - - # 最外层异常捕获,确保任何错误都能被记录并返回适当响应 - try: - # 1. 解析 XML - try: - root = ET.fromstring(xml_data) - except ET.ParseError as e: - logger.error(f"XML解析失败: {e}") - return self._gen_response_xml('FAIL', 'XML解析失败') - - # 2. 获取关键参数 - return_code = root.find('return_code').text if root.find('return_code') is not None else '' - result_code = root.find('result_code').text if root.find('result_code') is not None else '' - out_trade_no = root.find('out_trade_no').text if root.find('out_trade_no') is not None else '' - transaction_id = root.find('transaction_id').text if root.find('transaction_id') is not None else '' - - # 3. 签名验证 - if not self._verify_signature(xml_data): - logger.warning("签名验证失败") - return self._gen_response_xml('FAIL', '签名验证失败') - - # 4. 根据支付结果处理 - if return_code == 'SUCCESS' and result_code == 'SUCCESS': - self._handle_success(out_trade_no, transaction_id, root) - else: - logger.info(f"支付失败或异常,return_code={return_code}, result_code={result_code}") - self._handle_failure(out_trade_no) - - # 返回微信要求的成功响应(必须返回SUCCESS,即使内部处理失败也要返回,否则微信会重复通知) - return self._gen_response_xml('SUCCESS', 'OK') - - except Exception as e: - # 捕获任何未处理的异常,记录完整堆栈 - logger.error(f"回调处理发生未捕获异常: {str(e)}") - logger.error(traceback.format_exc()) - return self._gen_response_xml('FAIL', f'服务器内部错误: {str(e)}') - - def _verify_signature(self, xml_data): - """微信支付签名验证(MD5)""" - try: - root = ET.fromstring(xml_data) - sign = root.find('sign').text - # 构建待签名字符串(排除sign字段) - data = {child.tag: child.text for child in root if child.tag != 'sign'} - # 按key排序 - sorted_keys = sorted(data.keys()) - stringA = '&'.join([f"{k}={data[k]}" for k in sorted_keys]) - stringSignTemp = f"{stringA}&key={settings.WEIXIN_SHANGHUMIYAO}" - calc_sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() - logger.debug(f"计算签名: {calc_sign}, 微信签名: {sign}") - return calc_sign == sign - except Exception as e: - logger.error(f"签名验证过程异常: {e}") - logger.error(traceback.format_exc()) - return False - - def _gen_response_xml(self, return_code, return_msg): - """生成返回给微信的XML响应""" - xml = f""" - - -""" - logger.info(f"返回给微信的响应: {xml}") - return HttpResponse(xml, content_type='application/xml') - - @transaction.atomic - def _handle_success(self, out_trade_no, transaction_id, root): - """处理支付成功""" - logger.info(f"进入 _handle_success, out_trade_no={out_trade_no}, transaction_id={transaction_id}") - - # 锁定订单防止并发 - try: - dingdan = Dingdan.objects.select_for_update().filter(dingdan_id=out_trade_no).first() - except Exception as e: - logger.error(f"查询订单异常: {e}") - raise - - if not dingdan: - logger.error(f"订单不存在: {out_trade_no}") - return - - logger.info(f"订单当前状态: {dingdan.zhuangtai}") - - # 幂等处理:只有状态为9(未支付)才处理,已处理则跳过 - if dingdan.zhuangtai != 9: - logger.info(f"订单 {out_trade_no} 已处理过,状态={dingdan.zhuangtai},跳过") - return - - # 保存微信交易号 - if transaction_id: - dingdan.wechat_transaction_id = transaction_id - logger.info(f"保存微信交易号: {transaction_id}") - else: - logger.warning("微信未返回transaction_id") - - # 更新订单状态 - if dingdan.zhiding_id and dingdan.zhiding_id.strip(): - dingdan.zhuangtai = 7 - logger.info("指定单,状态设为7") - else: - dingdan.zhuangtai = 1 - logger.info("普通单,状态设为1") - - dingdan.save() - - logger.info(f"订单状态已更新为 {dingdan.zhuangtai}") - - # 删除同一用户同一商品的其他未支付订单(改为逻辑删除,状态25) - try: - abandoned_count = Dingdan.objects.filter( - user1_id=dingdan.user1_id, - shangpin_id=dingdan.shangpin_id, - zhuangtai=9 - ).exclude(dingdan_id=out_trade_no).update(zhuangtai=25) - logger.info(f"将 {abandoned_count} 个未支付订单标记为已废弃(25)") - except Exception as e: - logger.error(f"标记其他未支付订单异常: {e}") - - # 更新老板扩展表 - try: - laoban_id = None - if hasattr(dingdan, 'pingtai_kuozhan'): - laoban_id = dingdan.pingtai_kuozhan.laoban_id - if laoban_id: - user_main = UserMain.objects.filter(yonghuid=laoban_id).first() - if user_main and hasattr(user_main, 'boss_profile'): - boss = user_main.boss_profile - boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0) - boss.alldingdan = (boss.alldingdan or 0) + 1 - boss.save() - logger.info(f"更新老板 {laoban_id} 数据成功") - except Exception as e: - logger.error(f"更新老板数据异常: {e}") - - # 更新商品库存 - try: - if dingdan.shangpin_id: - Shangpin.objects.filter(id=dingdan.shangpin_id).update( - kucun=F('kucun') - 1 - ) - logger.info(f"商品 {dingdan.shangpin_id} 库存减1") - except Exception as e: - logger.error(f"更新商品库存异常: {e}") - - - - - # ========== 新增:商品和店铺每日统计 ========== - try: - # 商品每日统计(只要有商品ID就执行) - if dingdan.shangpin_id: - # 从扩展表获取已计算好的收益值(下单时已存储) - pingtai_shouyi = Decimal('0.00') - dianpu_shouyi = Decimal('0.00') - if hasattr(dingdan, 'pingtai_kuozhan'): - pingtai_shouyi = dingdan.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') - dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - - update_shangpin_daily_stat( - shangpin_id=dingdan.shangpin_id, - caozuo_leixing=1, # 1 = 下单 - dingdan_jiage=dingdan.jine, - pingtai_shouyi=pingtai_shouyi, - dianpu_zongshouyi=dianpu_shouyi - ) - logger.info(f"商品统计更新完成,商品ID: {dingdan.shangpin_id}") - - # 店铺每日统计(仅当存在店铺ID时执行) - dianpu_id = None - if hasattr(dingdan, 'pingtai_kuozhan'): - dianpu_id = dingdan.pingtai_kuozhan.dianpu_id - if dianpu_id: - dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - update_dianpu_daily_stat( - dianpu_id=dianpu_id, - caozuo_leixing=1, # 1 = 下单 - dingdan_jiage=dingdan.jine, - dianpu_shouyi=dianpu_shouyi - ) - logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") - except Exception as e: - logger.error(f"商品/店铺统计更新失败: {e}") - logger.error(traceback.format_exc()) - # 统计失败不影响主流程 - # ============================================= - - - - - - - - - # 更新收支记录(总累计) - self._update_szjilu(dingdan.jine) - - # 更新每日收入统计 - update_daily_income(dingdan.jine) - - # 跨平台同步 - 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, - '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) # 瞬间返回,不阻塞请求 - - # 异步广播通知 - #self._broadcast_async(dingdan) - - logger.info(f"订单 {out_trade_no} 支付成功处理完成") - - @transaction.atomic - def _handle_failure(self, out_trade_no): - """处理支付失败""" - logger.info(f"进入 _handle_failure, out_trade_no={out_trade_no}") - - try: - dingdan = Dingdan.objects.select_for_update().filter(dingdan_id=out_trade_no).first() - if not dingdan: - logger.warning(f"订单不存在: {out_trade_no}") - return - - logger.info(f"订单当前状态: {dingdan.zhuangtai}") - - # 仅当订单状态为9(未支付)时才标记失败,不直接删除 - if dingdan.zhuangtai == 9: - dingdan.zhuangtai = 14 # 自定义支付失败状态 - dingdan.save() - logger.info(f"订单 {out_trade_no} 支付失败,标记为14") - else: - logger.info(f"订单 {out_trade_no} 状态非9,不处理") - except Exception as e: - logger.error(f"处理支付失败异常: {e}") - logger.error(traceback.format_exc()) - - def _update_szjilu(self, jine): - """更新收支记录表(ID=1的记录)""" - try: - jine_decimal = Decimal(str(jine)) - with transaction.atomic(): - szjilu, created = Szjilu.objects.select_for_update().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') - } - ) - if created: - logger.info("创建ID=1的收支记录") - szjilu.zongsy += jine_decimal - szjilu.zongls += jine_decimal - szjilu.jrls += jine_decimal - szjilu.save() - logger.info(f"收支记录更新成功,增加金额: {jine}") - except Exception as e: - logger.error(f"更新收支记录失败: {e}") - - def _broadcast_async(self, dingdan): - """异步广播通知""" - 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), - 'amount': str(dingdan.jine), - 'order_desc': (dingdan.jieshao or '')[:50], - } - logger.info(f"准备广播订单: {order_info}") - sender = WeixinBroadcastSender() - result = sender.broadcast_order(order_info) - if result.get('success'): - logger.info(f"订单广播完成: {result.get('msg')}") - else: - logger.warning(f"订单广播异常: {result.get('msg')}") - except Exception as e: - logger.error(f"广播任务异常: {e}") - logger.error(traceback.format_exc()) - - thread = threading.Thread(target=task) - thread.daemon = True - thread.start() - logger.info("广播线程已启动") - except Exception as e: - logger.error(f"启动广播线程异常: {e}") - - 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 '游戏订单' - - - - -from django.core.cache import cache -class PaymentVerifyView(APIView): - """ - 订单支付成功复查接口 - 前端支付成功后调用,确认订单状态 - 当订单状态为9时,主动查询微信支付状态,若已支付则更新本地状态 - 需要token认证 - """ - permission_classes = [permissions.IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - try: - # ---------- 1. 获取参数 ---------- - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({'code': 1, 'msg': '订单ID不能为空'}) - - # ---------- 2. 查询订单 ---------- - dingdan = Dingdan.objects.filter(dingdan_id=dingdanid).first() - if not dingdan: - return Response({'code': 2, 'msg': '订单不存在'}) - - # ---------- 3. 验证订单归属(安全) ---------- - current_user = request.user - user_identifier = f"Boss{current_user.yonghuid}" - if dingdan.user1_id != user_identifier: - logger.warning(f"用户 {current_user.yonghuid} 尝试复查他人订单 {dingdanid}") - return Response({'code': 403, 'msg': '无权访问'}) - - # ---------- 4. 如果订单状态已是1/7,直接返回成功 ---------- - if dingdan.zhuangtai in [1, 7]: - return Response({ - 'code': 0, - 'msg': '订单状态正常', - 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai} - }) - - # ---------- 5. 如果状态为9,则主动查询微信支付状态 ---------- - if dingdan.zhuangtai == 9: - # 使用分布式锁防止并发处理同一订单 - lock_key = f"payment_verify_lock_{dingdanid}" - if not cache.add(lock_key, "locked", timeout=10): - # 已有其他进程在处理,让前端稍后重试 - return Response({'code': 1, 'msg': '订单正在处理中,请稍后'}) - - try: - # 调用微信订单查询接口 - trade_state, transaction_id = self._query_wechat_payment(dingdanid) - - if trade_state == 'SUCCESS': - # 支付成功,执行本地更新 - self._process_payment_success(dingdan, transaction_id, current_user.yonghuid) - # 重新获取订单以获取最新状态 - dingdan.refresh_from_db() - return Response({ - 'code': 0, - 'msg': '支付成功', - 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai} - }) - elif trade_state in ['NOTPAY', 'USERPAYING']: - # 用户尚未支付或支付中 - return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}}) - else: - # 其他状态:CLOSED, REVOKED, PAYERROR 等,视为支付失败 - return Response({'code': 2, 'msg': '支付失败'}) - - except Exception as e: - logger.error(f"查询微信支付异常: {e}", exc_info=True) - return Response({'code': 1, 'msg': '查询失败,请稍后重试'}) - finally: - cache.delete(lock_key) - else: - # ---------- 6. 其他异常状态 ---------- - status_msg = { - 2: '订单已取消', - 3: '订单已完成', - 4: '退款审核中', - 5: '已退款', - 6: '退款失败', - 8: '结算中', - 0: '订单已作废', - }.get(dingdan.zhuangtai, '订单状态异常') - return Response({'code': 3, 'msg': status_msg, 'data': {'zhuangtai': dingdan.zhuangtai}}) - - except Exception as e: - logger.error(f"订单复查接口异常: {e}", exc_info=True) - return Response({'code': 99, 'msg': f'系统错误: {str(e)}'}) - - def _process_payment_success(self, dingdan, transaction_id, operator_id): - """ - 支付成功后的原子操作(与微信回调逻辑完全一致) - 在事务内完成所有状态更新和数据一致性操作 - """ - with transaction.atomic(): - # 重新锁定订单,防止并发更新 - dingdan = Dingdan.objects.select_for_update().get(pk=dingdan.pk) - if dingdan.zhuangtai != 9: - # 订单状态已被其他进程更新,直接返回(幂等) - return - - # 更新订单状态 - dingdan.zhuangtai = 7 if dingdan.zhiding_id and dingdan.zhiding_id.strip() else 1 - dingdan.wechat_transaction_id = transaction_id - dingdan.clkf = operator_id # 可选:记录操作人ID - dingdan.save() - - # 删除同一用户同一商品的其他未支付订单(逻辑删除,状态25) - Dingdan.objects.filter( - user1_id=dingdan.user1_id, - shangpin_id=dingdan.shangpin_id, - zhuangtai=9 - ).exclude(dingdan_id=dingdan.dingdan_id).update(zhuangtai=25) - - # 更新老板扩展表 - laoban_id = dingdan.pingtai_kuozhan.laoban_id if hasattr(dingdan, 'pingtai_kuozhan') else None - if laoban_id: - user_main = UserMain.objects.filter(yonghuid=laoban_id).first() - if user_main and hasattr(user_main, 'boss_profile'): - boss = user_main.boss_profile - boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0) - boss.alldingdan = (boss.alldingdan or 0) + 1 - boss.save() - - # 更新商品库存 - if dingdan.shangpin_id: - Shangpin.objects.filter(id=dingdan.shangpin_id).update( - kucun=F('kucun') - 1 - ) - - - - # ========== 新增:商品和店铺每日统计 ========== - try: - # 商品每日统计(只要有商品ID就执行) - if dingdan.shangpin_id: - # 从扩展表获取已计算好的收益值(下单时已存储) - pingtai_shouyi = Decimal('0.00') - dianpu_shouyi = Decimal('0.00') - if hasattr(dingdan, 'pingtai_kuozhan'): - pingtai_shouyi = dingdan.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') - dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - - update_shangpin_daily_stat( - shangpin_id=dingdan.shangpin_id, - caozuo_leixing=1, # 1 = 下单 - dingdan_jiage=dingdan.jine, - pingtai_shouyi=pingtai_shouyi, - dianpu_zongshouyi=dianpu_shouyi - ) - logger.info(f"商品统计更新完成,商品ID: {dingdan.shangpin_id}") - - # 店铺每日统计(仅当存在店铺ID时执行) - dianpu_id = None - if hasattr(dingdan, 'pingtai_kuozhan'): - dianpu_id = dingdan.pingtai_kuozhan.dianpu_id - if dianpu_id: - dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - update_dianpu_daily_stat( - dianpu_id=dianpu_id, - caozuo_leixing=1, # 1 = 下单 - dingdan_jiage=dingdan.jine, - dianpu_shouyi=dianpu_shouyi - ) - logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") - except Exception as e: - logger.error(f"商品/店铺统计更新失败: {e}") - logger.error(traceback.format_exc()) - # 统计失败不影响主流程 - # ============================================= - - - - - - - - - # 更新收支记录 - self._update_szjilu(dingdan.jine) - - # 更新每日收入统计 - update_daily_income(dingdan.jine) - - # 跨平台同步(复查成功也需要通知) - sync_order_to_partners(dingdan.dingdan_id) - - def _query_wechat_payment(self, out_trade_no): - """ - 调用微信支付订单查询接口,返回 (trade_state, transaction_id) - 若查询失败,抛出异常 - """ - appid = settings.WEIXIN_APPID - mch_id = settings.WEIXIN_MCHID - key = settings.WEIXIN_SHANGHUMIYAO - nonce_str = self._generate_nonce_str() - - params = { - 'appid': appid, - 'mch_id': mch_id, - 'out_trade_no': out_trade_no, - 'nonce_str': nonce_str, - } - - # 生成签名 - 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/pay/orderquery' - response = requests.post(url, data=xml_data, timeout=10) - result = xmltodict.parse(response.content)['xml'] - - if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': - return result.get('trade_state'), result.get('transaction_id') - else: - err_msg = result.get('return_msg', '未知错误') - raise Exception(f"微信订单查询失败: {err_msg}") - - def _update_szjilu(self, jine): - """更新收支记录表(ID=1的记录)""" - try: - jine_decimal = Decimal(str(jine)) - with transaction.atomic(): - szjilu, created = Szjilu.objects.select_for_update().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') - } - ) - if created: - logger.info("创建ID=1的收支记录") - szjilu.zongsy += jine_decimal - szjilu.zongls += jine_decimal - szjilu.jrls += jine_decimal - szjilu.save() - logger.info(f"收支记录更新成功,增加金额: {jine}") - except Exception as e: - logger.error(f"更新收支记录失败: {e}") - # 不抛出异常,因为收支记录不是核心流程,但记录错误供排查 - - def _generate_nonce_str(self): - """生成随机字符串,长度32""" - return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - - def _dict_to_xml(self, data): - """将字典转换为XML字符串""" - xml = [''] - for k, v in data.items(): - xml.append(f'<{k}>{v}') - xml.append('') - 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): - """ - 创建订单并调用微信支付接口 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - # 1. 获取前端传递的字段 - shangpin_id = request.data.get('shangpin_id') - nicheng = request.data.get('nicheng', '').strip() - beizhu = request.data.get('beizhu', '').strip() - zhiding = request.data.get('zhiding', '').strip() - jine = request.data.get('jine', 0) - - # 2. 验证必填字段 - if not shangpin_id: - return Response({'code': 1, 'msg': '商品ID不能为空', 'data': None}) - - if not nicheng: - return Response({'code': 2, 'msg': '游戏昵称不能为空', 'data': None}) - - if len(nicheng) > 50: - return Response({'code': 3, 'msg': '游戏昵称过长', 'data': None}) - - # 3. 查询商品信息 - shangpin = Shangpin.objects.filter(id=shangpin_id).first() - if not shangpin: - return Response({'code': 4, 'msg': '商品不存在', 'data': None}) - - - is_valid, err_msg, _ = validate_shangpin_and_dianpu(shangpin_id) - if not is_valid: - return Response({'code': 5, 'msg': err_msg, 'data': None}) - - # 检查库存 - if hasattr(shangpin, 'kucun') and shangpin.kucun <= 0: - return Response({'code': 5, 'msg': '商品库存不足', 'data': None}) - - # 获取商品价格 - shangpin_jiage = getattr(shangpin, 'jiage', 0) - if not shangpin_jiage: - return Response({'code': 6, 'msg': '商品价格异常', 'data': None}) - #获取商品图片 - shangpin_tupian = getattr(shangpin, 'tupian_url', None) - - # 4. 验证指定打手(如果存在) - zhiding_id_value = None - if zhiding: - # 查询主表中是否存在该打手 - zhiding_user = UserMain.objects.filter( - yonghuid=zhiding, - #user_type='dashou' - ).first() - - if not zhiding_user: - return Response({'code': 7, 'msg': '指定打手不存在', 'data': None}) - - # 查询打手扩展表 - dashou_profile = UserDashou.objects.filter(user=zhiding_user).first() - - if not dashou_profile: - return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None}) - - # 检查在线状态 - if getattr(dashou_profile, 'zhanghaozhuangtai', 0) == 0: - return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None}) - - zhiding_id_value = zhiding - - # 5. 生成订单ID - timestamp = int(time.time()) - timestamp_ns = int(time.time_ns() // 1000) % 1000000 - random_num = random.randint(0, 99) - dingdanid = f"PT{timestamp:011d}{timestamp_ns:06d}{random_num:02d}" - - # 6. 查询利率并计算分成 - # 查询打手利率(字符'1') - dashou_lilu = Lilubiao.objects.filter(fadanpingtai='1').first() - - - # 🔥 新增:判断是否使用商品配置的额外分成 - if shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None: - # 使用商品指定的额外分成金额 - dashou_fencheng = shangpin.ewai_dashou_fencheng - else: - # 按原有逻辑计算打手分成 - dashou_fencheng = Decimal(str(shangpin_jiage)) * Decimal(str(dashou_lilu.lilu)) - - - - # 查询管事利率(字符'2') - #guanshi_lilu = Lilubiao.objects.filter(fadanpingtai='2').first() - - if not dashou_lilu: #or not guanshi_lilu: - return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None}) - - # 计算打手分成 - #dashou_fencheng = Decimal(str(shangpin_jiage)) * Decimal(str(dashou_lilu.lilu)) - - - - # 开始数据库事务 - with transaction.atomic(): - # 获取当前用户信息 - current_user = request.user - laoban_yonghuid = getattr(current_user, 'yonghuid', '') - user_openid = getattr(current_user, 'openid', '') - - if not user_openid: - return Response({'code': 10, 'msg': '用户openid不存在', 'data': None}) - - # 创建订单主表数据 - order_data = { - 'dingdan_id': dingdanid, - 'zhuangtai': 9, # 未付款状态 - 'jine': shangpin_jiage, - 'fadan_pingtai':1, - 'dashou_fencheng': dashou_fencheng, - 'zhiding_id': zhiding_id_value, - 'shangpin_id': shangpin_id, - 'yaoqiuleixing': getattr(shangpin, 'yaoqiuleixing', None), - 'huiyuan_id': getattr(shangpin, 'huiyuan_id', ''), - 'yongjin': getattr(shangpin, 'yongjin', 0), - 'jieshao': getattr(shangpin, 'jieshao', ''), - 'beizhu': beizhu, - 'nicheng': nicheng, - 'tupian':shangpin_tupian, - 'user1_id':f"Boss{request.user.yonghuid}", - 'leixing_id':shangpin.leixing_id - } - - # 创建订单记录 - - dingdan = Dingdan.objects.create(**order_data) - - - - - # CreateOrderView.post 事务内部,替换原有的扩展表创建代码 - - dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong = calculate_pingtai_and_dianpu_shouyi( - shangpin_id=shangpin_id, - dingdan_jiage=shangpin_jiage, - dashou_fencheng=dashou_fencheng - ) - - pingtaikuozhan_data = { - 'dingdan': dingdan, - 'laoban_id': laoban_yonghuid, - 'pingtai_shouyi': pingtai_shouyi, - # ---------- 新增上级分红快照 ---------- - 'shangji_dianpu_id': shangji_dianpu_id, - 'shangji_fenhong': shangji_fenhong if shangji_fenhong else Decimal('0.00'), - } - if dianpu_id: - pingtaikuozhan_data['dianpu_id'] = dianpu_id - pingtaikuozhan_data['dianpu_shouyi'] = dianpu_shouyi - else: - pingtaikuozhan_data['dianpu_shouyi'] = Decimal('0.00') - - DingdanPingtai.objects.create(**pingtaikuozhan_data) - - - - - # 计算收益 - '''dianpu_id, pingtai_shouyi, dianpu_shouyi = calculate_pingtai_and_dianpu_shouyi( - shangpin_id=shangpin_id, - dingdan_jiage=shangpin_jiage, - dashou_fencheng=dashou_fencheng - ) - - # 构建扩展表数据,平台收益必定保存,店铺收益和店铺ID仅当dianpu_id存在时才赋值 - pingtaikuozhan_data = { - 'dingdan': dingdan, - 'laoban_id': laoban_yonghuid, - 'pingtai_shouyi': pingtai_shouyi, - } - if dianpu_id: - pingtaikuozhan_data['dianpu_id'] = dianpu_id - pingtaikuozhan_data['dianpu_shouyi'] = dianpu_shouyi - else: - # 无店铺时,店铺收益字段保持默认(0.00),也可显式设为0 - pingtaikuozhan_data['dianpu_shouyi'] = Decimal('0.00') - - DingdanPingtai.objects.create(**pingtaikuozhan_data)''' - - - - - # 7. 生成微信支付参数(向微信官方发送请求) - pay_params = self.generate_wechat_pay_params( - dingdanid=dingdanid, - jine=shangpin_jiage, - beizhu=getattr(settings, 'WEIXIN_PAY_DESCRIPTION'), - user_ip=self.get_client_ip(request), - openid=user_openid - ) - - if not pay_params: - # 手动抛出异常触发事务回滚 - raise Exception('支付参数生成失败') - - # 8. 返回成功响应 - response_data = { - 'code': 0, - 'msg': '订单创建成功', - 'data': { - 'dingdanid': dingdanid, - 'payParams': pay_params - } - } - - return Response(response_data) - - except Exception as e: - # 这里会自动回滚事务 - return Response({'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None}) - - 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] - else: - ip = request.META.get('REMOTE_ADDR') - return ip - - def generate_wechat_pay_params(self, dingdanid, jine, beizhu, user_ip, openid): - """ - 调用微信支付统一下单接口,生成支付参数 - 完全按照您提供的正确示例代码编写 - """ - try: - # 微信支付配置 - APPID = getattr(settings, 'WEIXIN_APPID', '') - MCHID = getattr(settings, 'WEIXIN_MCHID', '') - KEY = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') - NOTIFY_URL = getattr(settings, 'WEIXIN_NOTIFY_URL', '') - - if not all([APPID, MCHID, KEY, NOTIFY_URL]): - raise Exception('微信支付配置不完整') - - # 生成随机字符串 - import string - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - - # 金额(单位:分) - total_fee = int(float(jine) * 100) - - # 交易类型 - trade_type = 'JSAPI' - - # 生成签名 - sign_data = { - 'appid': APPID, - 'mch_id': MCHID, - 'nonce_str': nonce_str, - 'body': beizhu, - 'out_trade_no': dingdanid, - 'total_fee': total_fee, - 'spbill_create_ip': user_ip, - 'notify_url': NOTIFY_URL, - 'trade_type': trade_type, - 'openid': openid - } - - # 按照参数名ASCII码从小到大排序 - 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}' - - # MD5签名 - sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - # 构造XML数据 - xml_data = f""" -{APPID} -{beizhu} -{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 - ) - - # 解析返回的XML - 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}' - - # 生成支付签名 - pay_sign_data = { - 'appId': APPID, - 'timeStamp': timestamp, - 'nonceStr': nonce_str, - 'package': package, - 'signType': 'MD5' - } - - 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() - - # 返回支付参数(格式与前端完全匹配) - result = { - 'timeStamp': timestamp, - 'nonceStr': nonce_str, - 'package': package, - 'signType': 'MD5', - 'paySign': pay_sign - } - - return result - else: - # 获取详细的错误信息 - err_code = root.find('err_code') - err_code_des = root.find('err_code_des') - return_msg = root.find('return_msg') - - 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}, " - if return_msg is not None: - error_details += f"return_msg: {return_msg.text}" - - raise Exception(f"微信支付下单失败: {error_details}") - - except Exception as e: - raise e - - - - - -'''# dingdan/views.py -import xml.etree.ElementTree as ET -from django.db import transaction -from django.http import HttpResponse -from rest_framework.views import APIView -from rest_framework import permissions - -from shangpin.models import Shangpin - -from yonghu.models import UserMain, UserBoss - -# views/pay_views.py - WechatPayNotifyView完整代码 -import xml.etree.ElementTree as ET -from django.db import transaction -from django.http import HttpResponse -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 peizhi.models import Szjilu -import logging - -logger = logging.getLogger(__name__) - - -import threading -from utils.weixin_broadcast import WeixinBroadcastSender - -class WechatPayNotifyView(APIView): - """微信支付回调接口 - 接收微信官方支付结果通知""" - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - - def post(self, request): - try: - # 1. 解析微信回调的XML数据 - xml_data = request.body.decode('utf-8') - root = ET.fromstring(xml_data) - - # 2. 获取回调参数 - return_code = root.find('return_code').text - result_code = root.find('result_code').text - out_trade_no = root.find('out_trade_no').text # 订单ID - - # 3. 验证签名(重要安全步骤) - if not self.verify_signature(request.body): - response_xml = self.generate_response_xml('FAIL', '签名验证失败') - return HttpResponse(response_xml, content_type='application/xml') - - # 4. 检查支付结果 - if return_code == 'SUCCESS' and result_code == 'SUCCESS': - # 支付成功,处理订单 - self.handle_payment_success(out_trade_no) - response_xml = self.generate_response_xml('SUCCESS', 'OK') - return HttpResponse(response_xml, content_type='application/xml') - else: - # 支付失败,删除订单 - self.handle_payment_failure(out_trade_no) - response_xml = self.generate_response_xml('SUCCESS', 'OK') - return HttpResponse(response_xml, content_type='application/xml') - - except Exception as e: - logger.error(f"支付回调处理异常: {str(e)}", exc_info=True) - response_xml = self.generate_response_xml('FAIL', f'处理失败: {str(e)}') - return HttpResponse(response_xml, content_type='application/xml') - - def verify_signature(self, xml_data): - """验证微信回调签名""" - try: - # 这里需要实现微信支付签名验证逻辑 - # 根据你的微信支付配置实现具体验证 - return True # 暂时返回True,你需要根据实际情况实现 - except: - return False - - def generate_response_xml(self, return_code, return_msg): - """生成返回给微信的XML响应""" - return f""" - - -""" - - @transaction.atomic - def handle_payment_success(self, dingdan_id): - """处理支付成功的订单""" - try: - # 1. 查询订单信息 - dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() - if not dingdan: - raise Exception('订单不存在') - - # 2. 检查订单状态是否为9(未支付) - if dingdan.zhuangtai != 9: - return - - # 3. 查询订单扩展表获取老板ID - pingtaikuozhan = DingdanPingtai.objects.filter(dingdan=dingdan).first() - if not pingtaikuozhan: - raise Exception('订单扩展表不存在') - - laoban_id = pingtaikuozhan.laoban_id - - # 4. 根据是否指定打手更新订单状态 - if dingdan.zhiding_id and dingdan.zhiding_id.strip(): - dingdan.zhuangtai = 7 - else: - dingdan.zhuangtai = 1 - - dingdan.save() - - # 5. 更新老板扩展表 - user_main = UserMain.objects.filter(yonghuid=laoban_id).first() - if user_main: - boss_profile = UserBoss.objects.filter(user=user_main).first() - if boss_profile: - boss_profile.zonge = (boss_profile.zonge or 0) + (dingdan.jine or 0) - boss_profile.alldingdan = (boss_profile.alldingdan or 0) + 1 - boss_profile.save() - - # 6. 更新商品库存和销量 - shangpin_id = dingdan.shangpin_id - if shangpin_id: - shangpin = Shangpin.objects.filter(id=shangpin_id).first() - if shangpin: - current_kucun = getattr(shangpin, 'kucun', 0) - if current_kucun > 0: - shangpin.kucun = current_kucun - 1 - shangpin.save() - - jine = dingdan.jine - self.update_szjilu(jine) - - - - 🟢【7. 新增】支付成功后的广播通知(异步,不阻塞主流程) - from dingdan.tongzhi_tasks import dingdan_guangbo - - # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用) - """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)""" # 瞬间返回,不阻塞请求 - - # 🟢【7. 新增】支付成功后的广播通知(异步,不阻塞主流程) - #self.broadcast_order_notification_async(dingdan) - - except Exception as e: - raise Exception(f'处理支付成功订单失败: {str(e)}') - - def broadcast_order_notification_async(self, dingdan): - """异步广播订单通知给所有关注用户""" - try: - def broadcast_task(): - 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[:50] if dingdan.jieshao else '新订单' - } - - # 调用广播器 - 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=broadcast_task) - thread.daemon = True - thread.start() - - except Exception as e: - logger.error(f"启动广播线程异常: {str(e)}") - - # 🗑️【重要】原 send_notification_to_dashou 和 send_notification_to_all_dashous 方法不再需要 - # 可以注释掉或删除,因为现在统一用 broadcast_order_notification_async - - def get_game_type_name(self, leixing_id): - """获取游戏类型名称(保持不变)""" - try: - game_type = ShangpinLeixing.objects.filter(id=leixing_id).first() - return game_type.jieshao if game_type else '游戏订单' - except Exception: - return '游戏订单' - - def update_szjilu(self, jine): - """更新收支记录表(保持不变)""" - try: - from decimal import Decimal - jine_decimal = Decimal(str(jine)) if not isinstance(jine, Decimal) else jine - - with transaction.atomic(): - try: - szjilu = Szjilu.objects.select_for_update().get(id=1) - except Szjilu.DoesNotExist: - szjilu = Szjilu.objects.create( - id=1, - zongsy=Decimal('0.00'), - zongls=Decimal('0.00'), - zongzc=Decimal('0.00'), - jrzc=Decimal('0.00'), - jrls=Decimal('0.00') - ) - logger.info(f"创建ID=1的收支记录") - - szjilu.zongsy = szjilu.zongsy + jine_decimal - szjilu.zongls = szjilu.zongls + jine_decimal - szjilu.jrls = szjilu.jrls + jine_decimal - szjilu.save() - - logger.info(f"收支记录更新成功: 订单金额={jine}") - - except Exception as e: - logger.error(f"更新收支记录失败: {str(e)}") - - @transaction.atomic - def handle_payment_failure(self, dingdan_id): - """处理支付失败的订单(删除订单)(保持不变)""" - try: - dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() - if not dingdan: - return - - DingdanPingtai.objects.filter(dingdan=dingdan).delete() - dingdan.delete() - - except Exception as e: - raise Exception(f'删除支付失败订单失败: {str(e)}')''' - - - - - -# dingdan/views.py -'''class PaymentFailView(APIView): - """ - 前端支付失败接口 - 处理用户主动取消或支付失败 - 需要token认证 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - # 1. 获取前端传递的字段 - dingdanid = request.data.get('dingdanid') - - - # 2. 验证必填字段 - if not dingdanid: - return Response({ - 'code': 1, - 'msg': '订单ID不能为空', - 'data': None - }) - - # 3. 查询订单 - dingdan = Dingdan.objects.filter(dingdan_id=dingdanid).first() - if not dingdan: - return Response({ - 'code': 2, - 'msg': '订单不存在', - 'data': None - }) - - # 4. 删除订单数据 - with transaction.atomic(): - # 删除订单扩展表 - DingdanPingtai.objects.filter(dingdan=dingdan).delete() - - # 删除订单主表 - dingdan.delete() - - # 5. 返回成功响应(与前端格式匹配) - return Response({ - 'code': 0, - 'msg': '订单已删除', - 'data': None - }) - - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'删除订单失败: {str(e)}', - 'data': None - })''' - - -# dingdan/views.py -'''class PaymentVerifyView(APIView): - """ - 订单支付成功复查接口 - 前端支付成功后调用,确认订单状态 - 需要token认证 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - # 1. 获取前端传递的订单ID - - dingdanid = request.data.get('dingdanid') - - - # 2. 验证必填字段 - if not dingdanid: - return Response({ - 'code': 1, - 'msg': '订单ID不能为空', - 'data': None - }) - - # 3. 查询订单 - dingdan = Dingdan.objects.filter(dingdan_id=dingdanid).first() - if not dingdan: - return Response({ - 'code': 2, - 'msg': '订单不存在', - 'data': None - }) - - # 4. 检查订单状态 - zhuangtai = dingdan.zhuangtai - - if zhuangtai in [1, 7]: - # 订单状态正常(1:待接单, 7:已指定打手待接单) - return Response({ - 'code': 0, - 'msg': '订单状态正常', - 'data': { - 'dingdanid': dingdanid, - 'zhuangtai': zhuangtai - } - }) - else: - # 订单状态异常 - error_msg = '订单状态异常,请联系管理员或客服' - - # 可以根据具体状态给出更详细的提示 - if zhuangtai == 9: - error_msg = '订单未支付,请重新支付' - elif zhuangtai == 2: - error_msg = '订单已取消' - elif zhuangtai == 3: - error_msg = '订单已完成' - # 其他状态... - - return Response({ - 'code': 3, - 'msg': error_msg, - 'data': { - 'dingdanid': dingdanid, - 'zhuangtai': zhuangtai - } - }) - - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'订单复查失败: {str(e)}', - 'data': None - })''' - - - -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): - """订单获取接口""" - permission_classes = [IsAuthenticated] - - def post(self, request): - # 获取当前登录用户 - try: - current_user = request.user - yonghuid = current_user.yonghuid - except Exception as e: - return Response({ - 'code': 1, - 'msg': '用户认证失败', - 'data': None - }) - - # 获取前端参数 - try: - data = request.data - zhuangtai_list = data.get('zhuangtai_list', []) - page = int(data.get('page', 1)) - page_size = int(data.get('page_size', 10)) - except Exception as e: - return Response({ - 'code': 2, - 'msg': '参数格式错误', - 'data': None - }) - - # 参数验证 - if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0: - return Response({ - 'code': 3, - 'msg': '状态列表不能为空', - 'data': None - }) - - if page < 1: - page = 1 - if page_size <= 0: - page_size = 10 - - try: - # 查询扩展表,获取用户的所有订单ID - pingtai_orders = DingdanPingtai.objects.filter(laoban_id=yonghuid) - pingtai_order_ids = list(pingtai_orders.values_list('dingdan__dingdan_id', flat=True)) - - if not pingtai_order_ids: - return Response({ - 'code': 0, - 'msg': '成功', - 'data': { - 'list': [], - 'has_more': False, - 'total': 0 - } - }) - - # 查询主表 - status_query = Q() - if zhuangtai_list != [1, 2, 3, 4, 5, 6, 7, 8]: - status_query = Q(zhuangtai__in=zhuangtai_list) - - query = Q(dingdan_id__in=pingtai_order_ids) & status_query - orders = Dingdan.objects.filter(query).order_by('-create_time') - #total_orders = orders.count() - - # 手动分页逻辑 - start_index = (page - 1) * page_size - end_index = start_index + page_size - - # 获取当前页的数据 - current_page_orders = orders[start_index:end_index] - - # 构建返回数据 - order_list = [] - for order in current_page_orders: - order_data = { - 'dingdanId': order.dingdan_id, - 'zhuangtai': order.zhuangtai, - 'jine': float(order.jine) if order.jine else 0.0, - 'tupian': order.tupian or '', - 'jieshao': order.jieshao or '', - 'beizhu': order.beizhu or '', - 'nicheng': order.nicheng or '', - 'chuangjianshijian': order.create_time.strftime('%Y-%m-%d %H:%M:%S') - } - order_list.append(order_data) - - - # 判断是否还有更多数据 - current_page_count = len(current_page_orders) - has_more = current_page_count == page_size - - return Response({ - 'code': 0, - 'msg': '成功', - 'data': { - 'list': order_list, - 'has_more': has_more, - #'total': total_orders - } - }) - - except Exception as e: - return Response({ - 'code': 4, - 'msg': '服务器内部错误', - 'data': None - }) - - -# 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): - """订单详情接口""" - permission_classes = [IsAuthenticated] - - def post(self, request): - # 获取当前登录用户 - try: - current_user = request.user - yonghuid = current_user.yonghuid - except Exception as e: - return Response({ - 'code': 1, - 'msg': '用户认证失败', - 'data': None - }) - - # 获取前端参数 - try: - data = request.data - dingdan_id = data.get('dingdanId', '') - except Exception as e: - return Response({ - 'code': 2, - 'msg': '参数格式错误', - 'data': None - }) - - # 参数验证 - if not dingdan_id: - return Response({ - 'code': 3, - 'msg': '订单ID不能为空', - 'data': None - }) - - try: - # 1. 验证订单是否属于当前用户 - # 查询平台订单扩展表,检查laoban_id是否等于当前用户 - pingtai_kuozhan = DingdanPingtai.objects.filter( - laoban_id=yonghuid, - dingdan__dingdan_id=dingdan_id - ).first() - - if not pingtai_kuozhan: - # 如果不是平台订单,检查商家订单 - from .models import DingdanShangjia - shangjia_kuozhan = DingdanShangjia.objects.filter( - shangjia_id=yonghuid, - dingdan__dingdan_id=dingdan_id - ).first() - - if not shangjia_kuozhan: - return Response({ - 'code': 4, - 'msg': '订单不存在或无权限查看', - 'data': None - }) - - # 2. 查询订单主表 - dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() - - if not dingdan: - return Response({ - 'code': 5, - 'msg': '订单不存在', - 'data': None - }) - - # 3. 获取订单信息 - zhuangtai = dingdan.zhuangtai or 0 - zhiding_id = dingdan.zhiding_id or '' - jiedan_dashou_id = dingdan.jiedan_dashou_id or '' - dashou_liuyan = dingdan.dashou_liuyan or '' - - # 4. 查询打手交付图片 - dashoutupian_records = Dashoutupian.objects.filter( - dingdan_id=dingdan_id - ).order_by('create_time')[:9] # 最多取9条 - - dashoujiaofu_list = [] - for record in dashoutupian_records: - if record.tupian: - dashoujiaofu_list.append(record.tupian) - - # 5. 构建返回数据(字段名与前端对应) - response_data = { - 'zhuangtai': zhuangtai, # 订单状态 - 'zhiding': zhiding_id, # 指定打手ID - 'dashou': jiedan_dashou_id, # 服务打手ID - 'dashouliuyan': dashou_liuyan, # 打手留言 - 'dashoujiaofu': dashoujiaofu_list # 打手交付图片列表 - } - - # 6. 返回成功响应 - return Response({ - 'code': 0, - 'msg': '成功', - 'data': response_data - }) - - except Exception as e: - print(f"订单详情查询失败: {str(e)}") - return Response({ - 'code': 6, - 'msg': '服务器内部错误', - 'data': None - }) - - - - - - -class JiedanView(APIView): - """结单接口""" - permission_classes = [IsAuthenticated] - - def post(self, request): - # 获取当前登录用户 - try: - current_user = request.user - yonghuid = current_user.yonghuid - except Exception as e: - return Response({ - 'code': 1, - 'msg': '用户认证失败', - 'data': None - }) - - # 获取前端参数 - try: - data = request.data - dingdan_id = data.get('dingdanId', '') - pingfen = int(data.get('pingfen', 0)) # 评分,默认0 - liuyan = data.get('liuyan', '') # 留言,默认空 - except Exception as e: - return Response({ - 'code': 2, - 'msg': '参数格式错误', - 'data': None - }) - - # 参数验证 - if not dingdan_id: - return Response({ - 'code': 3, - 'msg': '订单ID不能为空', - 'data': None - }) - - # 验证评分范围 - if pingfen < 0 or pingfen > 5: - return Response({ - 'code': 4, - 'msg': '评分必须在0-5分之间', - 'data': None - }) - - # 使用事务确保数据一致性 - try: - with transaction.atomic(): - # 1. 验证订单是否存在且属于当前用户 - pingtai_kuozhan = DingdanPingtai.objects.filter( - laoban_id=yonghuid, - dingdan__dingdan_id=dingdan_id - ).select_related('dingdan').first() - - if not pingtai_kuozhan: - return Response({ - 'code': 5, - 'msg': '订单不存在或无权限操作', - 'data': None - }) - - dingdan = pingtai_kuozhan.dingdan - - # 2. 验证订单状态(必须是结算中 - 状态8) - if dingdan.zhuangtai != 8: - return Response({ - 'code': 6, - 'msg': '订单不在结算状态,无法结单', - 'data': None - }) - - # 3. 获取订单相关数据 - dashou_fencheng = dingdan.dashou_fencheng or Decimal('0.00') # 打手分成 - jiedan_dashou_id = dingdan.jiedan_dashou_id or '' # 接单打手ID - shangpin_id = dingdan.shangpin_id # 商品ID - - # 4. 更新平台扩展表的老板评价(如果有留言) - if liuyan: - pingtai_kuozhan.laoban_pingjia = liuyan - pingtai_kuozhan.save() - - # 5. 如果有评分,更新评分表 - if pingfen > 0 and jiedan_dashou_id: - # 使用filter查询,更健壮 - pingfen_records = Pingfen.objects.filter(dashou=jiedan_dashou_id) - - if pingfen_records.exists(): - # 已有记录,更新 - pingfen_record = pingfen_records.first() - pingfen_record.zongfen += Decimal(str(pingfen)) - pingfen_record.cishu += 1 - pingfen_record.save() - else: - # 新记录,创建 - Pingfen.objects.create( - dashou=jiedan_dashou_id, - zongfen=Decimal(str(pingfen)), - cishu=1 - ) - - # 6. 更新商品真实销量(如果有商品ID) - if shangpin_id: - shangpin_records = Shangpin.objects.filter(id=shangpin_id) - - if shangpin_records.exists(): - shangpin = shangpin_records.first() - # 检查字段是否存在 - if hasattr(shangpin, 'zhenshi_xiaoliang'): - shangpin.zhenshi_xiaoliang = (shangpin.zhenshi_xiaoliang or 0) + 1 - shangpin.save() - - # ========== 新增:跨平台我方派单且有对方信息,直接通知对方并跳过本地结算 ========== - if (dingdan.is_cross == 1 - and dingdan.partner_order_id - and dingdan.partner_club_id): - # 更新订单状态为已完成 - dingdan.zhuangtai = 3 - dingdan.clkf = current_user.phone if hasattr(current_user, 'phone') else yonghuid - dingdan.save() - - # 事务提交后异步通知对方平台 - transaction.on_commit( - lambda: self._notify_partner_jiedan( - my_dingdan_id=dingdan.dingdan_id, - partner_club_id=dingdan.partner_club_id, - partner_order_id=dingdan.partner_order_id, - new_status=3, - current_user=current_user - ) - ) - - # 直接返回,跳过后续所有本地打手结算、评价、统计等 - return Response({ - 'code': 0, - 'msg': '结单成功(已通知对方平台)', - 'data': None - }) - # ==================================================================== - - # 7. 更新打手扩展表(如果有打手ID和分成) - if jiedan_dashou_id and dashou_fencheng > 0: - # 查询打手用户 - dashou_user = UserMain.objects.filter(yonghuid=jiedan_dashou_id).first() - - if dashou_user: - # 查询打手扩展表 - dashou_kuozhan = UserDashou.objects.filter(user=dashou_user).first() - - if dashou_kuozhan: - # 更新四个金额字段 - dashou_kuozhan.zonge = (dashou_kuozhan.zonge or Decimal('0.00')) + dashou_fencheng - dashou_kuozhan.yue = (dashou_kuozhan.yue or Decimal('0.00')) + dashou_fencheng - dashou_kuozhan.jinrishouyi = (dashou_kuozhan.jinrishouyi or Decimal( - '0.00')) + dashou_fencheng - dashou_kuozhan.jinyueshouyi = (dashou_kuozhan.jinyueshouyi or Decimal( - '0.00')) + dashou_fencheng - - # 成交总量加1 - dashou_kuozhan.chengjiaozongliang = (dashou_kuozhan.chengjiaozongliang or 0) + 1 - - # 状态改为数字1 - dashou_kuozhan.zhuangtai = 1 - - dashou_kuozhan.save() - - # 8. 更新订单状态为已完成(状态3) - dingdan.zhuangtai = 3 - dingdan.save() - - # 🔥 新增:调用打手每日统计(成交) - try: - update_dashou_daily_by_action( - yonghuid=jiedan_dashou_id, - amount=dashou_fencheng, - action=2 # 2 表示成交(结算) - ) - logger.info(f"打手 {jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - # 不影响主流程 - - - # ========== 新增:商品和店铺每日统计 ========== - try: - # 商品每日统计(只要有商品ID就执行) - if dingdan.shangpin_id: - # 从扩展表获取已计算好的收益值(下单时已存储) - pingtai_shouyi = Decimal('0.00') - dianpu_shouyi = Decimal('0.00') - if hasattr(dingdan, 'pingtai_kuozhan'): - pingtai_shouyi = dingdan.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') - dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - - update_shangpin_daily_stat( - shangpin_id=dingdan.shangpin_id, - caozuo_leixing=2, # 2 = 结单 - dingdan_jiage=dingdan.jine, - pingtai_shouyi=pingtai_shouyi, - dianpu_zongshouyi=dianpu_shouyi - ) - logger.info(f"商品统计更新完成,商品ID: {dingdan.shangpin_id}") - - # 店铺每日统计(仅当存在店铺ID时执行) - - - # ========== 修改为 ========== - # 店铺每日统计(包含上级分红) - dianpu_id = None - shangji_id = None - shangji_fen = Decimal('0.00') - if hasattr(dingdan, 'pingtai_kuozhan'): - dianpu_id = dingdan.pingtai_kuozhan.dianpu_id - shangji_id = dingdan.pingtai_kuozhan.shangji_dianpu_id - shangji_fen = dingdan.pingtai_kuozhan.shangji_fenhong or Decimal('0.00') - if dianpu_id: - dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - update_dianpu_daily_stat( - dianpu_id=dianpu_id, - caozuo_leixing=2, # 结算 - dingdan_jiage=dingdan.jine, - dianpu_shouyi=dianpu_shouyi, - shangji_dianpu_id=shangji_id, # 新增:上级店铺ID - shangji_fenhong=shangji_fen # 新增:该单给上级的分红 - ) - logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}, 上级ID: {shangji_id}, 上级分红: {shangji_fen}") - - - - - '''dianpu_id = None - if hasattr(dingdan, 'pingtai_kuozhan'): - dianpu_id = dingdan.pingtai_kuozhan.dianpu_id - if dianpu_id: - dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - update_dianpu_daily_stat( - dianpu_id=dianpu_id, - caozuo_leixing=2, # 2 = 结单 - dingdan_jiage=dingdan.jine, - dianpu_shouyi=dianpu_shouyi - ) - logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")''' - except Exception as e: - logger.error(f"商品/店铺统计更新失败: {e}") - logger.error(traceback.format_exc()) - # 统计失败不影响主流程 - # ============================================= - - # 12. 更新平台收益流水(仅在平台发单时) - if dingdan.fadan_pingtai == 1: - try: - from dingdan.utils import update_platform_profit # 导入函数 - update_platform_profit(dingdan_id) - except Exception as e: - # 平台收益更新失败不影响主流程,但记录日志 - logger.error(f"强制结单后更新平台收益失败: {str(e)}") - - # 9. 返回成功响应 - return Response({ - 'code': 0, - 'msg': '结单成功', - 'data': None - }) - - except Exception as e: - - # 事务会自动回滚 - return Response({ - 'code': 7, - 'msg': '结单失败,服务器内部错误', - 'data': None - }) - - - def _notify_partner_jiedan(self, my_dingdan_id, partner_club_id, partner_order_id, new_status, current_user): - """ - 通知对方俱乐部我方订单已完成 - 参照 CrossPlatformForceCompleteView._notify_partner_order_status 实现 - """ - try: - # 1. 获取我方俱乐部ID - from .models import CrossPlatformOrderData - 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 - - # 2. 查询对方服务器域名 - 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 - - # 3. 构造通知请求 - url = partner_domain.rstrip('/') + '/houtai/dfddtk' - payload = { - 'order_id': partner_order_id, # 在对方看来这是他们的订单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: - # 4. 更新跨平台订单扩展表(记录对方状态) - CrossPlatformOrderData.objects.update_or_create( - dingdan_id=my_dingdan_id, # 我方订单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') - } - ) - 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)}") - - - - - - - -from dengji.models import DashouBiaoxian -from dengji.services import record_dashou_biaoxian - - - - - - -class DingdanXiangqingView2(APIView): - """订单详情接口(老板端)- 返回完整评价信息""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - yonghuid = request.user.yonghuid - except Exception: - return Response({'code': 1, 'msg': '用户认证失败', 'data': None}) - - dingdan_id = request.data.get('dingdanId', '') - if not dingdan_id: - return Response({'code': 2, 'msg': '订单ID不能为空', 'data': None}) - - try: - # 权限验证 - pingtai_ext = DingdanPingtai.objects.filter( - laoban_id=yonghuid, - dingdan__dingdan_id=dingdan_id - ).select_related('dingdan').first() - - if not pingtai_ext: - return Response({'code': 3, 'msg': '订单不存在或无权限查看', 'data': None}) - - order = pingtai_ext.dingdan - zhuangtai = order.zhuangtai or 0 - jiedan_dashou_id = order.jiedan_dashou_id or '' - dashou_liuyan = order.dashou_liuyan or '' - update_time = order.update_time.strftime('%Y-%m-%d %H:%M:%S') if order.update_time else '' - - # 交付图片 - dashoujiaofu_list = list( - Dashoutupian.objects.filter(dingdan_id=dingdan_id) - .order_by('create_time')[:9] - .values_list('tupian', flat=True) - ) - - # 打手信息 - dashou_nicheng = '' - dashou_touxiang = '' - jiedan_time = '' - tijiao_time = '' - if jiedan_dashou_id: - dashou_user = UserMain.objects.filter(yonghuid=jiedan_dashou_id).first() - if dashou_user: - dashou_touxiang = dashou_user.avatar or '' - try: - dashou_nicheng = dashou_user.dashou_profile.nicheng or '' - except UserDashou.DoesNotExist: - pass - biaoxian = DashouBiaoxian.objects.filter( - dingdan_id=dingdan_id, - dashou_id=jiedan_dashou_id - ).first() - if biaoxian: - if biaoxian.jiedan_time: - jiedan_time = biaoxian.jiedan_time.strftime('%Y-%m-%d %H:%M:%S') - if biaoxian.tijiao_time: - tijiao_time = biaoxian.tijiao_time.strftime('%Y-%m-%d %H:%M:%S') - - # 老板自己的评价(如果已经结算过,可能已有评价) - laoban_pingjia = pingtai_ext.laoban_pingjia or '' - shangxian_sudu = None - fuwu_taidu = None - fanche_cishu = None - if jiedan_dashou_id and zhuangtai in [3, 4, 5, 6]: # 已完成或退款相关状态才可能有评价 - biaoxian = DashouBiaoxian.objects.filter( - dingdan_id=dingdan_id, - dashou_id=jiedan_dashou_id - ).first() - if biaoxian: - shangxian_sudu = biaoxian.shangxian_sudu - fuwu_taidu = biaoxian.fuwu_taidu - fanche_cishu = biaoxian.fanche_cishu - - data = { - 'zhuangtai': zhuangtai, - 'dashou': jiedan_dashou_id, - 'dashou_nicheng': dashou_nicheng, - 'dashou_touxiang': dashou_touxiang, - 'dashouliuyan': dashou_liuyan, - 'dashoujiaofu': dashoujiaofu_list, - 'update_time': update_time, - 'jiedan_time': jiedan_time, - 'tijiao_time': tijiao_time, - 'laoban_pingjia': laoban_pingjia, - 'shangxian_sudu': shangxian_sudu, # 1-5 或 null - 'fuwu_taidu': fuwu_taidu, # 1-5 或 null - 'fanche_cishu': fanche_cishu # int 或 null - } - - return Response({'code': 0, 'msg': '成功', 'data': data}) - - except Exception as e: - logger.error(f"订单详情查询异常: {e}", exc_info=True) - return Response({'code': 4, 'msg': '服务器内部错误', 'data': None}) - - - -class JiedanView2(APIView): - """结单接口(老板端)- 接收全新评价字段并写入表现表""" - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 用户认证 - try: - current_user = request.user - yonghuid = current_user.yonghuid - except Exception: - return Response({'code': 1, 'msg': '用户认证失败', 'data': None}) - - # 2. 获取参数(字段名与前端完全一致) - try: - data = request.data - dingdan_id = data.get('dingdanId', '') - fanche_cishu = data.get('fanche_cishu') # 前端传的字符串或数字,后端转为整数 - shangxian_sudu = data.get('shangxian_sudu') - fuwu_taidu = data.get('fuwu_taidu') - liuyan = data.get('liuyan', '') - except Exception: - return Response({'code': 2, 'msg': '参数格式错误', 'data': None}) - - # 参数校验 - if not dingdan_id: - return Response({'code': 3, 'msg': '订单ID不能为空', 'data': None}) - - # 转换并校验必填评价字段 - try: - fanche_cishu = int(fanche_cishu) if fanche_cishu is not None else None - shangxian_sudu = int(shangxian_sudu) if shangxian_sudu is not None else None - fuwu_taidu = int(fuwu_taidu) if fuwu_taidu is not None else None - except (ValueError, TypeError): - return Response({'code': 4, 'msg': '评价参数必须为整数', 'data': None}) - - if fanche_cishu is None: - return Response({'code': 5, 'msg': '翻车次数不能为空', 'data': None}) - if not (1 <= shangxian_sudu <= 5 and 1 <= fuwu_taidu <= 5): - return Response({'code': 6, 'msg': '评价分数必须在1-5之间', 'data': None}) - - # 使用事务保证数据一致性 - try: - with transaction.atomic(): - # 3. 权限验证:订单属于当前老板 - pingtai_ext = DingdanPingtai.objects.filter( - laoban_id=yonghuid, - dingdan__dingdan_id=dingdan_id - ).select_related('dingdan').first() - - if not pingtai_ext: - return Response({'code': 7, 'msg': '订单不存在或无权限操作', 'data': None}) - - order = pingtai_ext.dingdan - - # 4. 状态校验:必须是结算中(状态8) - if order.zhuangtai != 8: - return Response({'code': 8, 'msg': '订单不在结算状态', 'data': None}) - - # 5. 提取订单关键数据 - dashou_fencheng = order.dashou_fencheng or Decimal('0.00') - jiedan_dashou_id = order.jiedan_dashou_id or '' - shangpin_id = order.shangpin_id - - # 6. 保存老板评价(兼容旧逻辑) - if liuyan: - pingtai_ext.laoban_pingjia = liuyan - pingtai_ext.save() - - # 7. 更新评分表(用服务态度分作为综合评分,保留原有统计) - if jiedan_dashou_id and fuwu_taidu: - pingfen_records = Pingfen.objects.filter(dashou=jiedan_dashou_id) - if pingfen_records.exists(): - pf = pingfen_records.first() - pf.zongfen += Decimal(str(fuwu_taidu)) - pf.cishu += 1 - pf.save() - else: - Pingfen.objects.create( - dashou=jiedan_dashou_id, - zongfen=Decimal(str(fuwu_taidu)), - cishu=1 - ) - - # 8. 更新商品真实销量 - if shangpin_id: - shangpin_qs = Shangpin.objects.filter(id=shangpin_id) - if shangpin_qs.exists(): - sp = shangpin_qs.first() - if hasattr(sp, 'zhenshi_xiaoliang'): - sp.zhenshi_xiaoliang = (sp.zhenshi_xiaoliang or 0) + 1 - sp.save() - - # 9. 更新打手收益与状态 - if jiedan_dashou_id and dashou_fencheng > 0: - dashou_user = UserMain.objects.filter(yonghuid=jiedan_dashou_id).first() - if dashou_user: - try: - dashou_ext = dashou_user.dashou_profile - dashou_ext.zonge = (dashou_ext.zonge or Decimal('0.00')) + dashou_fencheng - dashou_ext.yue = (dashou_ext.yue or Decimal('0.00')) + dashou_fencheng - dashou_ext.jinrishouyi = (dashou_ext.jinrishouyi or Decimal('0.00')) + dashou_fencheng - dashou_ext.jinyueshouyi = (dashou_ext.jinyueshouyi or Decimal('0.00')) + dashou_fencheng - dashou_ext.chengjiaozongliang = (dashou_ext.chengjiaozongliang or 0) + 1 - dashou_ext.zhuangtai = 1 - dashou_ext.save() - except UserDashou.DoesNotExist: - pass - - # 10. 更新订单状态为已完成 - order.zhuangtai = 3 - order.save() - - # 11. 打手每日统计(成交) - try: - update_dashou_daily_by_action( - yonghuid=jiedan_dashou_id, - amount=dashou_fencheng, - action=2 # 2=结算/成交 - ) - except Exception as e: - logger.error(f"打手每日统计失败: {e}") - - # 12. 商品/店铺每日统计 - try: - if 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=shangpin_id, - caozuo_leixing=2, - dingdan_jiage=order.jine, - pingtai_shouyi=pingtai_shouyi, - dianpu_zongshouyi=dianpu_shouyi - ) - - # 店铺统计(含上级分红) - if hasattr(order, 'pingtai_kuozhan'): - dianpu_id = order.pingtai_kuozhan.dianpu_id - shangji_id = order.pingtai_kuozhan.shangji_dianpu_id - shangji_fen = order.pingtai_kuozhan.shangji_fenhong or Decimal('0.00') - if dianpu_id: - update_dianpu_daily_stat( - dianpu_id=dianpu_id, - caozuo_leixing=2, - dingdan_jiage=order.jine, - dianpu_shouyi=dianpu_shouyi, - shangji_dianpu_id=shangji_id, - shangji_fenhong=shangji_fen - ) - except Exception as e: - logger.error(f"商品/店铺统计失败: {e}") - - # 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}") - - # 14. 🆕 写入打手表现表(结算评价) - if jiedan_dashou_id: - record_dashou_biaoxian( - dingdan_id=dingdan_id, - dashou_id=jiedan_dashou_id, - xingwei=3, # 3=结算 - fanche_cishu=fanche_cishu, - shangxian_sudu=shangxian_sudu, - fuwu_taidu=fuwu_taidu - # bankuai 暂不传入,后续可从订单商品关联 - ) - - # 15. 返回成功 - return Response({'code': 0, 'msg': '结单成功', 'data': None}) - - except Exception as e: - logger.error(f"结单事务异常: {e}", exc_info=True) - 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): - """ - 获取商家可用的商品类型列表(含关联板块的称号) - POST /dingdan/sjspleixing - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - - try: - # 1. 查询审核通过且抢单要求类型为1的商品类型,按排序权重升序 - shangpin_queryset = ShangpinLeixing.objects.filter( - yaoqiuleixing=1, - shenhezhuangtai=1 - ).select_related('bankuai').order_by('paixu') - - # 2. 收集所有涉及到的板块ID - bankuai_ids = shangpin_queryset.values_list('bankuai_id', flat=True).distinct() - - # 3. 查询这些板块下的所有称号(每个称号绑定到一个板块) - chenghao_list = Chenghao.objects.filter( - bankuai_id__in=bankuai_ids - ).values('id', 'mingcheng', 'texiao_miaoshu', 'bankuai_id') - - # 4. 构建 板块ID -> 称号列表 的映射(确保准确关联) - bankuai_chenghao_map = {} - for ch in chenghao_list: - bid = ch['bankuai_id'] - if bid not in bankuai_chenghao_map: - bankuai_chenghao_map[bid] = [] - bankuai_chenghao_map[bid].append({ - 'id': ch['id'], - 'mingcheng': ch['mingcheng'], - 'texiao_json': ch['texiao_miaoshu'] # 前端 chenghao-tag 组件需要 - }) - - # 5. 组装最终数据 - data_list = [] - for sp in shangpin_queryset: - item = { - 'id': sp.id, - 'yaoqiuleixing': sp.yaoqiuleixing, - 'huiyuan_id': sp.huiyuan_id, - 'jieshao': sp.jieshao, - # 当前类型所属板块的称号列表(无板块则为空数组) - 'chenghaoList': bankuai_chenghao_map.get(sp.bankuai_id, []) - } - data_list.append(item) - - return Response({ - 'code': 200, - 'msg': '获取成功', - 'data': data_list - }) - - 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 - - - - - -'''class ShangjiaPaifaView(APIView): - """商家派单接口""" - permission_classes = [IsAuthenticated] - - def post(self, request): - # 记录请求日志 - logger.info(f"商家派单请求: user_id={request.user.yonghuid}, data={request.data}") - - # 1. 获取前端参数 - data = request.data - shangpin_type_id = data.get('shangpinTypeId') - huiyuan_id = data.get('huiyuanId') - yaoqiuleixing = data.get('yaoqiuleixing') - dingdan_jieshao = data.get('dingdanJieshao', '').strip() - dingdan_beizhu = data.get('dingdanBeizhu', '').strip() - laoban_name = data.get('laobanName', '').strip() - zhiding_uid = data.get('zhidingUid', '').strip() - jiage_str = data.get('jiage') - - # 2. 基本参数验证 - if not all([shangpin_type_id, huiyuan_id, dingdan_jieshao, laoban_name, jiage_str]): - logger.warning(f"参数缺失: {request.data}") - return Response({'code': 400, 'msg': '缺少必填参数', 'data': None}, status=status.HTTP_400_BAD_REQUEST) - - try: - jiage = float(jiage_str) - if jiage < 1 or jiage > 10000: - return Response({'code': 400, 'msg': '价格需在1-10000元之间', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - if '.' in jiage_str and len(jiage_str.split('.')[1]) > 2: - return Response({'code': 400, 'msg': '最多支持两位小数', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - except ValueError: - return Response({'code': 400, 'msg': '价格格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST) - - # 3. 获取商家扩展信息 - try: - shangjia = request.user.shop_profile - except UserShangjia.DoesNotExist: - logger.warning(f"用户不是商家: user_id={request.user.yonghuid}") - return Response({'code': 403, 'msg': '您不是商家或商家信息未完善', 'data': None}, - status=status.HTTP_403_FORBIDDEN) - - # 4. 验证商家状态 - if shangjia.zhuangtai != 1: - logger.warning(f"商家状态异常: user_id={request.user.yonghuid}, status={shangjia.zhuangtai}") - return Response({'code': 403, 'msg': '商家账号状态异常,请联系管理员', 'data': None}, - status=status.HTTP_403_FORBIDDEN) - - # 5. 验证商家余额 - if jiage > shangjia.yue: - logger.warning(f"余额不足: user_id={request.user.yonghuid}, 余额={shangjia.yue}, 需要={jiage}") - return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 6. 验证指定打手 - zhiding_dashou = None - dashou_user_main = None - if zhiding_uid: - try: - zhiding_user = UserMain.objects.get(yonghuid=zhiding_uid) - dashou_user_main = zhiding_user - - try: - zhiding_dashou = zhiding_user.dashou_profile - except UserDashou.DoesNotExist: - logger.warning(f"指定用户不是打手: zhiding_uid={zhiding_uid}") - return Response({'code': 400, 'msg': '指定用户不是打手', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - if zhiding_dashou.zhanghaozhuangtai != 1: - logger.warning( - f"指定打手状态异常: zhiding_uid={zhiding_uid}, status={zhiding_dashou.zhanghaozhuangtai}") - return Response({'code': 400, 'msg': '指定打手账号状态异常', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - except UserMain.DoesNotExist: - logger.warning(f"指定用户不存在: zhiding_uid={zhiding_uid}") - return Response({'code': 400, 'msg': '指定用户不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 7. 查询利率 - try: - lilu_obj = Lilubiao.objects.get(fadanpingtai='3') - lilu = float(lilu_obj.lilu) - if lilu <= 0: - lilu = 1 - except Lilubiao.DoesNotExist: - logger.warning("利率表不存在,使用默认值1") - lilu = 1 - - dashou_fencheng = round(jiage * lilu, 2) - - # 8. 生成订单ID - def generate_dingdan_id(): - import time - import random - 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}" - - # 9. 使用事务 - 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=int(yaoqiuleixing) if yaoqiuleixing else 1, - huiyuan_id=str(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}" - ) - - # 创建商家订单扩展表 - DingdanShangjia.objects.create( - dingdan=dingdan, - shangjia_id=request.user.yonghuid, - sjnicheng=request.user.boss_profile.nickname, - ) - - # 更新商家统计信息 - from django.db.models import 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, - ) - - # 🟢【10. 修改】发送模板消息 - 现在改为广播给所有用户 - from houtai.utils import update_shangjia_daily - - # ✅ 新方法(行为驱动,更安全) - update_shangjia_daily( - yonghuid=request.user.yonghuid, - amount=Decimal(str(jiage)), - action=1 # 1 = 派发 - ) - - - - - - - - - - #🟢【7. 新增】支付成功后的广播通知(异步,不阻塞主流程) - from dingdan.tongzhi_tasks import dingdan_guangbo - - # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用) - 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) # 瞬间返回,不阻塞请求 - - sync_order_to_partners(dingdan.dingdan_id) - - - - - - - - - #self.broadcast_order_notification_async(dingdan) - - # 记录成功日志 - logger.info(f"商家派单成功: order_id={dingdan.dingdan_id}, amount={jiage}, zhiding={zhiding_uid}") - - return Response({'code': 200, 'msg': '派单成功', 'data': {'jine': str(dingdan.jine)}}, - status=status.HTTP_200_OK) - - 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 broadcast_order_notification_async(self, dingdan): - """异步广播订单通知给所有关注用户""" - try: - def broadcast_task(): - 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[:50] if dingdan.jieshao else '新订单' - } - - # 调用广播器 - 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=broadcast_task) - thread.daemon = True - thread.start() - - except Exception as e: - logger.error(f"启动广播线程异常: {str(e)}") - - # 🗑️【重要】原 send_template_notification_async 方法不再需要 - # 可以注释掉或删除,因为现在统一用 broadcast_order_notification_async - - 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 '游戏订单''''' - - - -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] - - 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, 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_yongjin = None - 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: - final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1 - final_huiyuan_id = str(huiyuan_id) if huiyuan_id else '' - logger.warning("佣金为0,降级为会员模式") - except ValueError: - final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1 - final_huiyuan_id = str(huiyuan_id) if huiyuan_id else '' - logger.warning("佣金格式错误,降级为会员模式") - else: - final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1 - final_huiyuan_id = str(huiyuan_id) if huiyuan_id else '' - logger.info(f"会员模式: 类型={final_yaoqiu_type}") - - # ----- 7. 称号(可选:不传/空值/无效/不存在均视为无标签)----- - chenghao_obj = None - label_id_str = str(label_id).strip() if label_id is not None else '' - if label_id_str and label_id_str not in ('0', 'null', 'undefined'): - try: - lid = int(label_id_str) - if lid > 0: - chenghao_obj = Chenghao.objects.filter(id=lid).first() - if chenghao_obj: - logger.info(f"订单将关联称号: {chenghao_obj.mingcheng}") - else: - logger.info(f"标签不存在,按无标签派单: {label_id}") - except (ValueError, TypeError): - logger.info(f"标签ID无效,按无标签派单: {label_id}") - - # ----- 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. 利率(打手+管事分成,打手优先) ----- - 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) - 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, - guanshi_fencheng=guanshi_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) - - # Celery 广播任务(异步) - 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 '游戏订单' - -# 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__) - - - -'''class ShangjiaDingdanHuoquView(APIView): - """ - 商家订单获取接口 - POST /dingdan/shangjia/dingdan - 需要JWT认证 - 支持分页、状态筛选、精确搜索(订单ID或游戏昵称) - 返回待处理订单数量(pending_count) - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取当前登录用户的yonghuid - try: - current_user = request.user - yonghuid = current_user.yonghuid - except AttributeError: - return Response({ - 'code': 1, - 'msg': '用户认证失败', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 解析参数 - try: - data = request.data - zhuangtai_list = data.get('zhuangtai_list', []) - page = max(1, int(data.get('page', 1))) - page_size = min(100, max(1, int(data.get('page_size', 10)))) - - # 搜索参数(精确匹配) - search_type = data.get('search_type') # 可能的值:'order_id' 或 'nicheng' - search_value = data.get('search_value', '').strip() - except (ValueError, TypeError): - return Response({ - 'code': 2, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 验证状态列表 - if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0: - return Response({ - 'code': 3, - 'msg': '状态列表不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - try: - # 4. 构建基础查询(该商家的所有订单) - base_qs = DingdanShangjia.objects.filter( - shangjia_id=yonghuid - ).select_related('dingdan') - - # 5. 状态过滤(始终使用传入的zhuangtai_list) - if zhuangtai_list: - base_qs = base_qs.filter(dingdan__zhuangtai__in=zhuangtai_list) - - # 6. 搜索过滤(精确匹配) - if search_type and search_value: - if search_type == 'order_id': - # 订单ID精确匹配(dingdan_id有唯一索引) - base_qs = base_qs.filter(dingdan__dingdan_id=search_value) - elif search_type == 'nicheng': - # 游戏昵称精确匹配(建议为nicheng字段添加索引) - base_qs = base_qs.filter(dingdan__nicheng=search_value) - # 其他类型忽略 - - # 7. 计算待处理订单数量(可抢订单:状态为1或7) - pending_count = DingdanShangjia.objects.filter( - shangjia_id=yonghuid, - dingdan__zhuangtai__in=[1, 7] - ).count() - - # 8. 排序、分页 - total_count = base_qs.count() - start_index = (page - 1) * page_size - end_index = start_index + page_size - # 注意:这里使用切片会触发数据库LIMIT,性能良好 - current_page_orders = base_qs.order_by('-dingdan__create_time')[start_index:end_index] - - # 9. 构建返回数据 - order_list = [] - for shangjia_order in current_page_orders: - order = shangjia_order.dingdan - order_data = { - 'dingdan_id': order.dingdan_id, - 'zhuangtai': order.zhuangtai, - 'jine': float(order.jine) if order.jine else 0.0, - 'jieshao': order.jieshao or '', - 'beizhu': order.beizhu or '', - 'nicheng': order.nicheng or '', - 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '' - } - order_list.append(order_data) - - # 10. 判断是否还有更多 - current_page_count = len(current_page_orders) - has_more = current_page_count == page_size and end_index < total_count - - return Response({ - 'code': 0, - 'msg': '成功', - 'data': { - 'list': order_list, - 'has_more': has_more, - 'total': total_count, - 'pending_count': pending_count # 新增待处理数量 - } - }, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f"商家订单获取失败: {str(e)}", exc_info=True) - return Response({ - 'code': 4, - 'msg': '服务器内部错误', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' - - -# views.py 中 ShangjiaDingdanHuoquView 完整改造 - -class ShangjiaDingdanHuoquView(APIView): - """ - 商家订单获取接口 - POST /dingdan/sjdingdanhq - 需要JWT认证 - 支持分页、状态筛选、商品类型筛选、模糊搜索(订单ID/订单介绍/游戏昵称/打手ID) - 返回待结算订单数量(pending_count) - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取当前登录用户的yonghuid - try: - current_user = request.user - yonghuid = current_user.yonghuid - except AttributeError: - return Response({ - 'code': 1, - 'msg': '用户认证失败', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 解析参数 - try: - data = request.data - zhuangtai_list = data.get('zhuangtai_list', []) - page = max(1, int(data.get('page', 1))) - page_size = min(100, max(1, int(data.get('page_size', 10)))) - - # 🆕 商品类型筛选 - leixing_id = data.get('leixing_id', None) - - # 🆕 模糊搜索关键字(支持订单ID、订单介绍、游戏昵称、打手ID) - keyword = data.get('keyword', '').strip() - except (ValueError, TypeError): - return Response({ - 'code': 2, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 验证状态列表 - if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0: - return Response({ - 'code': 3, - 'msg': '状态列表不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - try: - # 4. 构建基础查询(该商家的所有订单) - base_qs = DingdanShangjia.objects.filter( - shangjia_id=yonghuid - ).select_related('dingdan') - - # 5. 状态过滤 - base_qs = base_qs.filter(dingdan__zhuangtai__in=zhuangtai_list) - - # 🆕 6. 商品类型筛选 - if leixing_id is not None: - try: - leixing_id_int = int(leixing_id) - base_qs = base_qs.filter(dingdan__leixing_id=leixing_id_int) - except (ValueError, TypeError): - # 如果leixing_id不是有效整数,忽略该筛选 - pass - - # 🆕 7. 模糊搜索(支持订单ID、订单介绍、游戏昵称、打手ID) - if keyword: - from django.db.models import Q - # 订单ID精确匹配(dingdan_id有唯一索引,先用精确匹配) - dingdan_id_match = Q(dingdan__dingdan_id__exact=keyword) - # 订单介绍模糊匹配 - jieshao_match = Q(dingdan__jieshao__icontains=keyword) - # 游戏昵称模糊匹配 - nicheng_match = Q(dingdan__nicheng__icontains=keyword) - # 打手ID精确匹配 - dashou_match = Q(dingdan__jiedan_dashou_id__exact=keyword) - - base_qs = base_qs.filter( - dingdan_id_match | jieshao_match | nicheng_match | dashou_match - ) - - # 8. 计算待结算订单数量(状态为8) - pending_count = DingdanShangjia.objects.filter( - shangjia_id=yonghuid, - dingdan__zhuangtai=8 - ).count() - - # 9. 排序、分页 - total_count = base_qs.count() - start_index = (page - 1) * page_size - end_index = start_index + page_size - current_page_orders = base_qs.order_by('-dingdan__create_time')[start_index:end_index] - - # 10. 构建返回数据 - order_list = [] - for shangjia_order in current_page_orders: - order = shangjia_order.dingdan - order_data = { - 'dingdan_id': order.dingdan_id, - 'zhuangtai': order.zhuangtai, - 'jine': float(order.jine) if order.jine else 0.0, - 'jieshao': order.jieshao or '', - 'beizhu': order.beizhu or '', - 'nicheng': order.nicheng or '', - 'leixing_id': order.leixing_id, # 🆕 返回商品类型ID - 'tupian': order.tupian or '', # 🆕 返回商品图片 - 'jiedan_dashou_id': order.jiedan_dashou_id or '', # 🆕 返回打手ID - 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '' - } - order_list.append(order_data) - - # 11. 判断是否还有更多 - current_page_count = len(current_page_orders) - has_more = current_page_count == page_size and end_index < total_count - - return Response({ - 'code': 0, - 'msg': '成功', - 'data': { - 'list': order_list, - 'has_more': has_more, - 'total': total_count, - 'pending_count': pending_count - } - }, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f"商家订单获取失败: {str(e)}", exc_info=True) - return Response({ - 'code': 4, - 'msg': '服务器内部错误', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - - - - - -'''class ShangjiaDingdanXiangqingView(APIView): - """商家订单详情接口 - 高性能实现""" - permission_classes = [IsAuthenticated] # JWT认证 - - def post(self, request): - # 1. 获取当前登录用户的yonghuid - try: - current_user = request.user - yonghuid = current_user.yonghuid - except AttributeError: - return Response({ - 'code': 1, - 'msg': '用户认证失败', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 获取前端参数 - try: - data = request.data - dingdan_id = data.get('dingdan_id', '').strip() - except Exception: - return Response({ - 'code': 2, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 参数验证 - if not dingdan_id: - return Response({ - 'code': 3, - 'msg': '订单ID不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - try: - # 4. 高性能查询:一次性获取所有关联数据 - # 使用select_related和prefetch_related避免N+1查询 - - # 先查询商家订单扩展表,验证订单权限 - shangjia_kuozhan = DingdanShangjia.objects.filter( - shangjia_id=yonghuid, - dingdan__dingdan_id=dingdan_id - ).select_related('dingdan').first() - - if not shangjia_kuozhan: - return Response({ - 'code': 4, - 'msg': '订单不存在或您无权限查看', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 5. 查询打手交付图片(使用prefetch_related可以一次性获取所有相关图片) - # 这里我们单独查询,因为是多对一关系 - dashou_tupian_list = Dashoutupian.objects.filter( - dingdan_id=dingdan_id - ).order_by('create_time').values_list('tupian', flat=True) - - # 转换为列表,确保即使为空也返回列表 - dashou_jiaofu = list(dashou_tupian_list) - - # 6. 构建返回数据 - 字段名必须与前端完全匹配 - # 注意:所有字段即使为空也要返回,前端会处理空值 - order = shangjia_kuozhan.dingdan - - response_data = { - # 基础字段 - 'dingdan_id': order.dingdan_id, - 'zhuangtai': order.zhuangtai or 0, - - # 服务相关信息 - 'fuwudashou_id': order.jiedan_dashou_id or '', # 服务打手ID - 'dashou_liuyan': order.dashou_liuyan or '', # 打手留言 - 'dashou_jiaofu': dashou_jiaofu, # 打手交付图片列表 - - # 商家订单扩展表字段 - 'shangjia_liuyan': shangjia_kuozhan.shangjia_pingjia or '', # 商家留言 - 'tkliyou': order.tkly or '', # 退款理由(暂时没有字段) - 'cfliyou': shangjia_kuozhan.cfliyou or '', # 处罚申请原因 - 'sqzhuangtai': shangjia_kuozhan.sqzhuangtai or 0, # 申请状态 - 'bhliyou': shangjia_kuozhan.bhliyou or '', # 驳回理由 - 'chufa_jieguo': '', # 处罚结果(暂时没有字段) - - # 其他可能需要的基础信息 - 'jine': float(order.jine) if order.jine else 0.0, - 'jieshao': order.jieshao or '', - 'beizhu': order.beizhu or '', - 'nicheng': order.nicheng or '', - 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S'), - - - } - - return Response({ - 'code': 0, - 'msg': '成功', - 'data': response_data - }, 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({ - 'code': 5, - 'msg': '服务器内部错误', - 'data': None - }, 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): - """ - 商家订单详情接口 - 返回订单基础信息、打手信息、交付图片、罚款数据、分红利率、更换打手产生的新订单ID等 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取当前登录用户的 yonghuid - try: - current_user = request.user - yonghuid = current_user.yonghuid - except AttributeError: - return Response({ - 'code': 1, - 'msg': '用户认证失败', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 获取前端参数 - try: - data = request.data - dingdan_id = data.get('dingdan_id', '').strip() - except Exception: - return Response({ - 'code': 2, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if not dingdan_id: - return Response({ - 'code': 3, - 'msg': '订单ID不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - try: - # 3. 查询商家订单扩展表,验证权限并获取基础订单数据 - shangjia_kuozhan = DingdanShangjia.objects.filter( - shangjia_id=yonghuid, - dingdan__dingdan_id=dingdan_id - ).select_related('dingdan').first() - - if not shangjia_kuozhan: - return Response({ - 'code': 4, - 'msg': '订单不存在或您无权限查看', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - order = shangjia_kuozhan.dingdan - - # 4. 交付图片列表 - dashou_tupian_list = Dashoutupian.objects.filter( - dingdan_id=dingdan_id - ).order_by('create_time').values_list('tupian', flat=True) - dashou_jiaofu = list(dashou_tupian_list) - - # 5. 打手详细信息(头像、昵称、用户ID) - dashou_yonghuid = order.jiedan_dashou_id or '' - dashou_nicheng = '' - dashou_avatar = '' - if dashou_yonghuid: - dashou_user = UserMain.objects.filter( - yonghuid=dashou_yonghuid - ).select_related('dashou_profile').first() - if dashou_user: - dashou_avatar = dashou_user.avatar or '' - if hasattr(dashou_user, 'dashou_profile') and dashou_user.dashou_profile: - dashou_nicheng = dashou_user.dashou_profile.nicheng or '' - - # 6. 罚款数据(如果存在) - fadan_obj = Fadan.objects.filter( - guanliandingdan_id=dingdan_id, - beichufa_id=dashou_yonghuid - ).first() - fadan_dict = None - if fadan_obj: - fadan_dict = { - 'id': fadan_obj.id, - 'chufaliyou': fadan_obj.chufaliyou or '', - 'fakuanjine': float(fadan_obj.fakuanjine) if fadan_obj.fakuanjine else 0.0, - 'yingxiang_qiangdan': fadan_obj.yingxiang_qiangdan, - 'zhuangtai': fadan_obj.zhuangtai, - 'shensuliyou': fadan_obj.shensuliyou or '', - 'tongyi_chufaliyou': fadan_obj.tongyi_chufaliyou or '', - 'bohuiliyou': fadan_obj.bohuiliyou or '', - 'create_time': fadan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if fadan_obj.create_time else '', - 'update_time': fadan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if fadan_obj.update_time else '', - } - - # 7. 商家自己的分红利率(用于罚款分红计算) - fenhong_lilv = 0.0 - try: - # 查找商家作为分红者的最新记录,取应得利率 - last_fenhong = FadanFenhong.objects.filter( - fenhongzhe_id=yonghuid, - shenfen=1 # 1=商家 - ).order_by('-create_time').first() - if last_fenhong: - fenhong_lilv = float(last_fenhong.yingde_lilv or 0) - except Exception as e: - logger.warning(f'获取分红利率失败: {e}') - - # 8. 更换打手后产生的新订单ID(如果当前订单被更换过) - xin_dingdan_id = '' - try: - # 查找以当前订单作为“上一个订单ID”的新订单扩展记录 - new_shangjia_order = DingdanShangjia.objects.filter( - shangyige_dingdan_id=dingdan_id, - shangjia_id=yonghuid - ).select_related('dingdan').first() - if new_shangjia_order: - xin_dingdan_id = new_shangjia_order.dingdan.dingdan_id - except Exception as e: - logger.warning(f'查找新订单ID失败: {e}') - - # 9. 构建返回数据 - response_data = { - # 基础订单字段 - 'dingdan_id': order.dingdan_id, - 'zhuangtai': order.zhuangtai or 0, - 'jine': float(order.jine) if order.jine else 0.0, - 'jieshao': order.jieshao or '', - 'beizhu': order.beizhu or '', - 'nicheng': order.nicheng or '', - 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '', - - # 打手信息 - 'dashou_yonghuid': dashou_yonghuid, - 'dashou_nicheng': dashou_nicheng, - 'dashou_avatar': dashou_avatar, - - # 服务详情 - 'fuwudashou_id': dashou_yonghuid, # 保留旧字段兼容 - 'dashou_liuyan': order.dashou_liuyan or '', - 'dashou_jiaofu': dashou_jiaofu, - - # 商家扩展字段(旧) - 'shangjia_liuyan': shangjia_kuozhan.shangjia_pingjia or '', - 'tkliyou': order.tkly or '', - 'cfliyou': shangjia_kuozhan.cfliyou or '', # 保留,但不影响新罚款 - 'sqzhuangtai': shangjia_kuozhan.sqzhuangtai or 0, - 'bhliyou': shangjia_kuozhan.bhliyou or '', - 'chufa_jieguo': '', # 已弃用 - - # 新增:罚款数据 - 'fadan': fadan_dict, - - # 新增:分红利率 - 'fenhong_lilv': fenhong_lilv, - - # 新增:更换打手后的新订单ID - 'xin_dingdan_id': xin_dingdan_id, - } - - return Response({ - 'code': 0, - 'msg': '成功', - 'data': response_data - }, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f"商家订单详情查询失败: {str(e)}", exc_info=True) - return Response({ - 'code': 5, - 'msg': '服务器内部错误', - 'data': None - }, 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] - - def post(self, request): - # 1. 获取当前登录用户 - try: - current_user = request.user - yonghuid = current_user.yonghuid - - try: - shangjia = current_user.shop_profile - except UserShangjia.DoesNotExist: - return Response({ - 'code': 1, - 'msg': '您不是商家或商家信息未完善', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - except AttributeError: - return Response({ - 'code': 1, - 'msg': '用户认证失败', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 获取前端参数 - try: - data = request.data - dingdan_id = data.get('dingdan_id', '').strip() - pingfen = int(data.get('pingfen', 0)) - liuyan = data.get('liuyan', '').strip() - except (ValueError, TypeError): - return Response({ - 'code': 2, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 参数验证 - if not dingdan_id: - return Response({ - 'code': 3, - 'msg': '订单ID不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if pingfen < 0 or pingfen > 5: - return Response({ - 'code': 4, - 'msg': '评分必须在0-5分之间', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - try: - with transaction.atomic(): - # 4. 查询订单并加锁(修复并发问题) - order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) - - # 验证订单是否属于当前商家 - shangjia_kuozhan = DingdanShangjia.objects.filter( - dingdan=order, - shangjia_id=yonghuid - ).first() - - if not shangjia_kuozhan: - return Response({ - 'code': 5, - 'msg': '订单不存在或您无权限操作', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 5. 验证订单状态 - if order.zhuangtai != 8: - return Response({ - 'code': 6, - 'msg': '订单不在结算状态,无法结单', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 获取订单字段 - jine = order.jine or Decimal('0') - dashou_fencheng = order.dashou_fencheng or Decimal('0') - jiedan_dashou_id = order.jiedan_dashou_id - dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单 - is_cross = order.is_cross - partner_club_id = order.partner_club_id - partner_order_id = order.partner_order_id - - if not jiedan_dashou_id: - return Response({ - 'code': 7, - 'msg': '订单没有接单打手,无法结单', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # ========== 跨平台判断 ========== - is_cross_claimed_by_partner = (is_cross == 1 and (partner_club_id or partner_order_id)) - - # ========== 根据订单类型处理 ========== - # 情况1:对方派单(我方打手接单)-> 预结算,不直接给打手加钱 - if dispatch_type == 2: - # 创建预结算记录 - PreSettlement.objects.update_or_create( - dingdan_id=dingdan_id, - defaults={ - 'partner_order_id': partner_order_id, - 'partner_club_id': partner_club_id or '000001', - 'dashou_id': jiedan_dashou_id, - 'amount': dashou_fencheng, - 'order_amount': jine, - 'status': 0, - 'operator': request.user.phone, - } - ) - # 更新订单状态 - order.zhuangtai = 3 - order.save() - - # 更新统计表 - update_daily_dispatch_stat( - direction=2, - partner_club_id=partner_club_id or '000001', - success_amount=jine, - success_count=1 - ) - - # 情况2:我方派单且被对方打手接单 -> 只改状态,不给本地打手加钱 - elif dispatch_type == 1 and is_cross_claimed_by_partner: - order.zhuangtai = 3 - order.save() - - update_daily_dispatch_stat( - direction=1, - partner_club_id=partner_club_id or '000001', - success_amount=dashou_fencheng, - success_count=1 - ) - - # 情况3:本地订单 或 跨平台但自家打手接单 -> 正常结算给打手 - else: - # 查询打手信息 - try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) - dashou = dashou_user.dashou_profile - except (UserMain.DoesNotExist, UserDashou.DoesNotExist): - return Response({ - 'code': 8, - 'msg': '接单打手不存在或信息不完整', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 原子更新打手数据(使用 F 表达式) - UserDashou.objects.filter(id=dashou.id).update( - chengjiaozongliang=F('chengjiaozongliang') + 1, - yue=F('yue') + dashou_fencheng, - zonge=F('zonge') + dashou_fencheng, - jinrishouyi=F('jinrishouyi') + dashou_fencheng, - jinyueshouyi=F('jinyueshouyi') + dashou_fencheng, - ) - - # 更新商家成交单量 - UserShangjia.objects.filter(id=shangjia.id).update( - chengjiao=F('chengjiao') + 1, - ) - - # 更新订单状态 - order.zhuangtai = 3 - order.save() - - # 处理评分(所有情况都处理) - if pingfen > 0: - pingfen_obj, created = Pingfen.objects.get_or_create( - dashou=jiedan_dashou_id, - defaults={'zongfen': pingfen, 'cishu': 1} - ) - if not created: - Pingfen.objects.filter(id=pingfen_obj.id).update( - zongfen=F('zongfen') + pingfen, - cishu=F('cishu') + 1, - ) - - # 更新商家订单扩展表的评价 - DingdanShangjia.objects.filter(id=shangjia_kuozhan.id).update( - shangjia_pingjia=liuyan, - ) - - # 🔥 新增:调用打手每日统计(成交) - try: - update_dashou_daily_by_action( - yonghuid=jiedan_dashou_id, - amount=dashou_fencheng, - action=2 # 2 表示成交(结算) - ) - logger.info(f"打手 {jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - # 不影响主流程 - - # ✅ 新方法(行为驱动,更安全) - update_shangjia_daily( - yonghuid=request.user.yonghuid, - amount=Decimal(str(jine)), - action=2 # 2 = 结算 - ) - - # 商家订单管事分红(仅 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) - - # 返回成功 - return Response({ - 'code': 0, - 'msg': '结单成功', - 'data': { - 'dingdan_id': dingdan_id, - 'new_zhuangtai': 3, - 'jine': float(jine), - 'dashou_fencheng': float(dashou_fencheng), - 'jiedan_dashou_id': jiedan_dashou_id, - 'pingfen': pingfen, - 'liuyan': liuyan - } - }, status=status.HTTP_200_OK) - - except Dingdan.DoesNotExist: - return Response({ - 'code': 5, - 'msg': '订单不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - logger.error(f"商家结单失败: {str(e)}", exc_info=True) - return Response({ - 'code': 9, - 'msg': '服务器内部错误,结单失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - -from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu - - -class ShangjiaChexiaoView(APIView): - """商家撤销订单申请接口""" - permission_classes = [IsAuthenticated] # JWT认证 - - def post(self, request): - # 1. 获取当前登录用户并验证商家身份 - try: - current_user = request.user - # 使用反向查询获取商家扩展信息(性能最佳) - shangjia = current_user.shop_profile - except UserShangjia.DoesNotExist: - return Response({ - 'code': 1, - 'msg': '您不是商家或商家信息未完善', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 2. 检查商家账号状态 - if shangjia.zhuangtai != 1: - return Response({ - 'code': 2, - 'msg': '商家账号状态异常,无法操作', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 3. 获取前端参数 - try: - data = request.data - dingdan_id = data.get('dingdan_id', '').strip() - chexiao_liyou = data.get('chexiao_liyou', '').strip() - except Exception: - return Response({ - 'code': 3, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 参数验证 - if not dingdan_id or not chexiao_liyou: - return Response({ - 'code': 4, - 'msg': '订单ID和撤销原因不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - try: - # 5. 使用事务确保数据一致性 - with transaction.atomic(): - # 查询订单主表 - dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() - if not dingdan: - return Response({ - 'code': 5, - 'msg': '订单不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 6. 验证订单状态 - if dingdan.zhuangtai not in [1, 7]: - return Response({ - 'code': 6, - 'msg': '当前订单状态不可撤销', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 7. 验证订单所属商家(使用反向查询) - shangjia_kuozhan = DingdanShangjia.objects.filter( - dingdan__dingdan_id=dingdan_id, - shangjia_id=current_user.yonghuid - ).first() - - if not shangjia_kuozhan: - return Response({ - 'code': 7, - 'msg': '您无权撤销此订单', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 8. 创建退款记录(注意:申请状态直接设为1-成功) - Tuikuanjilu.objects.create( - dingdan_id=dingdan_id, - qingqiuid=current_user.yonghuid, - liyou=chexiao_liyou, - sqzhuangtai=1, # 直接成功 - jine=dingdan.jine, - dashou_fencheng=dingdan.dashou_fencheng, - jieshao=dingdan.jieshao, - beizhu=dingdan.beizhu, - nicheng=dingdan.nicheng, - - ) - - # 9. 更新商家扩展表数据(使用F表达式原子操作) - UserShangjia.objects.filter(id=shangjia.id).update( - yue=F('yue') + dingdan.jine, - tuikuan=F('tuikuan') + 1, - jinridingdan=F('jinridingdan') - 1, - jinyuedingdan=F('jinyuedingdan') - 1, - jinriliushui=F('jinriliushui') - dingdan.jine, - jinyueliushui=F('jinyueliushui') - dingdan.jine, - - ) - - # 10. 更新订单状态为5(已退款) - dingdan.zhuangtai = 5 - - dingdan.save() - - # ✅ 新方法(行为驱动,更安全) - from houtai.utils import update_shangjia_daily - update_shangjia_daily( - yonghuid=request.user.yonghuid, - amount=Decimal(str(dingdan.jine)), - action=3 # 3 = 退款 - ) - - return Response({ - 'code': 0, - 'msg': '撤销成功', - 'data': { - 'dingdan_id': dingdan_id, - 'new_zhuangtai': 5, - 'tuikuan_liyou': chexiao_liyou, - 'jine': float(dingdan.jine) if dingdan.jine else 0.0 - } - }, 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({ - 'code': 8, - 'msg': '服务器内部错误,撤销失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - - - -class ShangjiaTuikuanShenqingView(APIView): - """商家退款申请接口""" - permission_classes = [IsAuthenticated] # JWT认证 - - def post(self, request): - # 1. 获取当前登录用户并验证商家身份 - try: - current_user = request.user - shangjia = current_user.shop_profile - except UserShangjia.DoesNotExist: - return Response({ - 'code': 1, - 'msg': '您不是商家或商家信息未完善', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 2. 检查商家账号状态 - if shangjia.zhuangtai != 1: - return Response({ - 'code': 2, - 'msg': '商家账号状态异常,无法操作', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 3. 获取前端参数 - try: - data = request.data - dingdan_id = data.get('dingdan_id', '').strip() - tuikuan_liyou = data.get('tuikuan_liyou', '').strip() - except Exception: - return Response({ - 'code': 3, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 参数验证 - if not dingdan_id or not tuikuan_liyou: - return Response({ - 'code': 4, - 'msg': '订单ID和退款原因不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - try: - # 5. 使用事务确保数据一致性 - with transaction.atomic(): - # 查询订单主表 - dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() - if not dingdan: - return Response({ - 'code': 5, - 'msg': '订单不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 6. 验证订单状态 - if dingdan.zhuangtai not in [2, 8]: - return Response({ - 'code': 6, - 'msg': '当前订单状态不可申请退款', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 7. 验证订单所属商家 - shangjia_kuozhan = DingdanShangjia.objects.filter( - dingdan__dingdan_id=dingdan_id, - shangjia_id=current_user.yonghuid - ).first() - - if not shangjia_kuozhan: - return Response({ - 'code': 7, - 'msg': '您无权为此订单申请退款', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # ========== 8. 处理退款记录(修改点:先查询是否存在) ========== - refund_record, created = Tuikuanjilu.objects.get_or_create( - dingdan_id=dingdan_id, - defaults={ - 'dashouid': dingdan.jiedan_dashou_id, - 'qingqiuid': current_user.yonghuid, - 'liyou': tuikuan_liyou, - 'sqzhuangtai': 0, # 待处理 - 'jine': dingdan.jine, - 'dashou_fencheng': dingdan.dashou_fencheng, - 'jieshao': dingdan.jieshao, - 'beizhu': dingdan.beizhu, - 'nicheng': dingdan.nicheng, - } - ) - if not created: - # 记录已存在,更新退款理由和申请时间 - refund_record.liyou = tuikuan_liyou - refund_record.save(update_fields=['liyou', 'update_time']) - logger.info(f"订单 {dingdan_id} 已有退款记录,已更新退款理由") - - # 9. 更新订单状态为4(退款中) - dingdan.zhuangtai = 4 - dingdan.tkly = tuikuan_liyou - dingdan.save() - - # ====== 原有功能:更新打手状态 ====== - if dingdan.jiedan_dashou_id: - try: - dashou_main = UserMain.objects.filter( - yonghuid=dingdan.jiedan_dashou_id - ).first() - if dashou_main and hasattr(dashou_main, 'dashou_profile'): - dashou_profile = dashou_main.dashou_profile - dashou_profile.zhuangtai = 1 - dashou_profile.save() - logger.info(f"退款申请成功,打手状态已更新:订单{dingdan_id},打手{dingdan.jiedan_dashou_id}状态变更为1") - except Exception as e: - logger.error(f"更新打手状态失败:{str(e)},订单{dingdan_id},打手ID{dingdan.jiedan_dashou_id}") - - # ========== 新增:如果是跨平台订单,异步通知对方平台 ========== - 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}" - payload = { - 'token': settings.CROSS_PLATFORM_TOKEN, - 'order_id': dingdan.dingdan_id - } - try: - resp = requests.post(url, json=payload, timeout=5) - if resp.status_code == 200: - data = resp.json() - if data.get('code') == 0: - logger.info(f"跨平台退款通知发送成功: {dingdan.dingdan_id}") - else: - logger.error(f"跨平台退款通知业务失败: {data}") - else: - logger.error(f"跨平台退款通知HTTP失败: {resp.status_code}") - except Exception as e: - logger.error(f"跨平台退款通知异常: {str(e)}", exc_info=True) - - thread = threading.Thread(target=send_refund_notify) - thread.daemon = True - thread.start() - logger.info(f"跨平台退款通知线程已启动: {dingdan.dingdan_id}") - # ========== 新增结束 ========== - - return Response({ - 'code': 0, - 'msg': '退款申请提交成功', - 'data': { - 'dingdan_id': dingdan_id, - 'new_zhuangtai': 4, - 'tuikuan_liyou': tuikuan_liyou, - 'jine': float(dingdan.jine) if dingdan.jine else 0.0, - 'dashou_id': dingdan.jiedan_dashou_id - } - }, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f"商家退款申请失败: {str(e)}", exc_info=True) - return Response({ - 'code': 8, - 'msg': '服务器内部错误,申请失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - -'''class ShangjiaTuikuanShenqingView(APIView): - """商家退款申请接口""" - permission_classes = [IsAuthenticated] # JWT认证 - - def post(self, request): - # 1. 获取当前登录用户并验证商家身份 - try: - current_user = request.user - shangjia = current_user.shop_profile - except UserShangjia.DoesNotExist: - return Response({ - 'code': 1, - 'msg': '您不是商家或商家信息未完善', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 2. 检查商家账号状态 - if shangjia.zhuangtai != 1: - return Response({ - 'code': 2, - 'msg': '商家账号状态异常,无法操作', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 3. 获取前端参数 - try: - data = request.data - dingdan_id = data.get('dingdan_id', '').strip() - tuikuan_liyou = data.get('tuikuan_liyou', '').strip() - except Exception: - return Response({ - 'code': 3, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 参数验证 - if not dingdan_id or not tuikuan_liyou: - return Response({ - 'code': 4, - 'msg': '订单ID和退款原因不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - try: - # 5. 使用事务确保数据一致性 - with transaction.atomic(): - # 查询订单主表 - dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() - if not dingdan: - return Response({ - 'code': 5, - 'msg': '订单不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 6. 验证订单状态 - if dingdan.zhuangtai not in [2, 8]: - return Response({ - 'code': 6, - 'msg': '当前订单状态不可申请退款', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 7. 验证订单所属商家 - shangjia_kuozhan = DingdanShangjia.objects.filter( - dingdan__dingdan_id=dingdan_id, - shangjia_id=current_user.yonghuid - ).first() - - if not shangjia_kuozhan: - return Response({ - 'code': 7, - 'msg': '您无权为此订单申请退款', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 8. 创建退款记录(申请状态为0-待处理) - Tuikuanjilu.objects.create( - dingdan_id=dingdan_id, - dashouid=dingdan.jiedan_dashou_id, - qingqiuid=current_user.yonghuid, - liyou=tuikuan_liyou, - sqzhuangtai=0, # 待处理 - jine=dingdan.jine, - dashou_fencheng=dingdan.dashou_fencheng, - jieshao=dingdan.jieshao, - beizhu=dingdan.beizhu, - nicheng=dingdan.nicheng, - - ) - - # 9. 更新订单状态为4(退款中) - dingdan.zhuangtai = 4 - dingdan.tkly = tuikuan_liyou - - dingdan.save() - - - # ====== 新增功能:更新打手状态 ====== - if dingdan.jiedan_dashou_id: - try: - # 高效查询:通过yonghuid查询用户主表,然后通过反向关系获取打手扩展表 - dashou_main = UserMain.objects.filter( - yonghuid=dingdan.jiedan_dashou_id - ).first() - - if dashou_main and hasattr(dashou_main, 'dashou_profile'): - # 通过OneToOneField反向关系直接获取打手扩展表 - dashou_profile = dashou_main.dashou_profile - # 将打手状态更新为1 - dashou_profile.zhuangtai = 1 - dashou_profile.save() - - # 记录日志 - import logging - logger = logging.getLogger(__name__) - logger.info( - f"退款申请成功,打手状态已更新:订单{dingdan_id},打手{dingdan.jiedan_dashou_id}状态变更为1") - except Exception as e: - # 打手状态更新失败不影响退款申请主流程,但记录错误日志 - import logging - logger = logging.getLogger(__name__) - logger.error(f"更新打手状态失败:{str(e)},订单{dingdan_id},打手ID{dingdan.jiedan_dashou_id}") - - return Response({ - 'code': 0, - 'msg': '退款申请提交成功', - 'data': { - 'dingdan_id': dingdan_id, - 'new_zhuangtai': 4, - 'tuikuan_liyou': tuikuan_liyou, - 'jine': float(dingdan.jine) if dingdan.jine else 0.0, - 'dashou_id': dingdan.jiedan_dashou_id - } - }, 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({ - 'code': 8, - 'msg': '服务器内部错误,申请失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' - - - -class ShangjiaChufaShenqingView(APIView): - """商家处罚申请接口(已添加图片上传功能)""" - permission_classes = [IsAuthenticated] # JWT认证 - - def post(self, request): - # 1. 获取当前登录用户并验证商家身份 - try: - current_user = request.user - shangjia = current_user.shop_profile - except UserShangjia.DoesNotExist: - return Response({ - 'code': 1, - 'msg': '您不是商家或商家信息未完善', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 2. 检查商家账号状态 - if shangjia.zhuangtai != 1: - return Response({ - 'code': 2, - 'msg': '商家账号状态异常,无法操作', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 3. 获取前端参数 - try: - data = request.data - dingdan_id = data.get('dingdan_id', '').strip() - dashou_id = data.get('dashou_id', '').strip() - chufa_liyou = data.get('chufa_liyou', '').strip() - chufa_tupian_urls = data.get('chufa_tupian_urls', []) # 🔴 新增:图片URL数组 - except Exception: - return Response({ - 'code': 3, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 参数验证 - if not dingdan_id or not dashou_id or not chufa_liyou: - return Response({ - 'code': 4, - 'msg': '订单ID、打手ID和处罚原因不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - try: - # 5. 使用事务确保数据一致性 - with transaction.atomic(): - # 验证打手身份 - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou = dashou_user.dashou_profile - except UserMain.DoesNotExist: - return Response({ - 'code': 5, - 'msg': '打手用户不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - except UserDashou.DoesNotExist: - return Response({ - 'code': 5, - 'msg': '该用户不是打手', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 查询订单主表 - dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() - if not dingdan: - return Response({ - 'code': 6, - 'msg': '订单不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 验证订单状态 - if dingdan.zhuangtai not in [2, 8, 4, 5, 6, 3]: - return Response({ - 'code': 7, - 'msg': '当前订单状态不可申请处罚', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证订单所属商家 - shangjia_kuozhan = DingdanShangjia.objects.filter( - dingdan__dingdan_id=dingdan_id, - shangjia_id=current_user.yonghuid - ).first() - - if not shangjia_kuozhan: - return Response({ - 'code': 8, - 'msg': '您无权为此订单申请处罚', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 验证订单的打手是否与提供的打手ID一致 - if dingdan.jiedan_dashou_id != dashou_id: - return Response({ - 'code': 9, - 'msg': '该打手并非此订单的服务打手', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 创建处罚记录 - chufajilu = Chufajilu.objects.create( - dashouid=dashou_id, - qingqiuid=current_user.yonghuid, - cfliyou=chufa_liyou, - sqzhuangtai=0, # 待处理 - jifen=5, # 扣除5积分 - dingdan_id=dingdan_id, - ) - # 🔴【新增】在创建处罚记录后立即扣除打手积分 - # 注意:这里使用前面已经获取的dashou对象(打手扩展表) - jifen_kouchu = chufajilu.jifen # 扣除的积分,这里是5 - if jifen_kouchu > 0: - # 检查打手积分是否足够 - if dashou.jifen < jifen_kouchu: - dashou.jifen = 0 - else: - dashou.jifen -= jifen_kouchu - dashou.save() - - # 🔴【新增】保存处罚图片到数据库 - if chufa_tupian_urls and isinstance(chufa_tupian_urls, list): - for tupian_url in chufa_tupian_urls: - if tupian_url and tupian_url.strip(): - Chufatupian.objects.create( - dingdan_id=dingdan_id, - yonghuid=current_user.yonghuid, - tupian=tupian_url - ) - - # 更新商家订单扩展表的处罚相关字段 - shangjia_kuozhan.cfliyou = chufa_liyou - shangjia_kuozhan.sqzhuangtai = 0 - shangjia_kuozhan.save() - - return Response({ - 'code': 0, - 'msg': '处罚申请提交成功', - 'data': { - 'dingdan_id': dingdan_id, - 'dashou_id': dashou_id, - 'chufa_liyou': chufa_liyou, - 'sqzhuangtai': 0, - 'jifen': 5, - 'tupian_count': len(chufa_tupian_urls) if chufa_tupian_urls else 0 # 🔴 返回图片数量 - } - }, 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({ - 'code': 10, - 'msg': '服务器内部错误,申请失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - -'''class ShangjiaChufaShenqingView(APIView): - """商家处罚申请接口""" - permission_classes = [IsAuthenticated] # JWT认证 - - def post(self, request): - # 1. 获取当前登录用户并验证商家身份 - try: - current_user = request.user - shangjia = current_user.shop_profile - except UserShangjia.DoesNotExist: - return Response({ - 'code': 1, - 'msg': '您不是商家或商家信息未完善', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 2. 检查商家账号状态 - if shangjia.zhuangtai != 1: - return Response({ - 'code': 2, - 'msg': '商家账号状态异常,无法操作', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 3. 获取前端参数 - try: - data = request.data - dingdan_id = data.get('dingdan_id', '').strip() - dashou_id = data.get('dashou_id', '').strip() - chufa_liyou = data.get('chufa_liyou', '').strip() - except Exception: - return Response({ - 'code': 3, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 参数验证 - if not dingdan_id or not dashou_id or not chufa_liyou: - return Response({ - 'code': 4, - 'msg': '订单ID、打手ID和处罚原因不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - try: - # 5. 使用事务确保数据一致性 - with transaction.atomic(): - # 验证打手身份 - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou = dashou_user.dashou_profile - except UserMain.DoesNotExist: - return Response({ - 'code': 5, - 'msg': '打手用户不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - except UserDashou.DoesNotExist: - return Response({ - 'code': 5, - 'msg': '该用户不是打手', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 查询订单主表 - dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() - if not dingdan: - return Response({ - 'code': 6, - 'msg': '订单不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 验证订单状态 - if dingdan.zhuangtai not in [2, 8,3,4,5,6]: - return Response({ - 'code': 7, - 'msg': '当前订单状态不可申请处罚', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证订单所属商家 - shangjia_kuozhan = DingdanShangjia.objects.filter( - dingdan__dingdan_id=dingdan_id, - shangjia_id=current_user.yonghuid - ).first() - - if not shangjia_kuozhan: - return Response({ - 'code': 8, - 'msg': '您无权为此订单申请处罚', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 验证订单的打手是否与提供的打手ID一致 - if dingdan.jiedan_dashou_id != dashou_id: - return Response({ - 'code': 9, - 'msg': '该打手并非此订单的服务打手', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 创建处罚记录 - Chufajilu.objects.create( - dashouid=dashou_id, - qingqiuid=current_user.yonghuid, - cfliyou=chufa_liyou, - sqzhuangtai=0, # 待处理 - jifen=5, # 扣除5积分 - dingdan_id=dingdan_id, - - ) - - # 更新商家订单扩展表的处罚相关字段 - shangjia_kuozhan.cfliyou = chufa_liyou - shangjia_kuozhan.sqzhuangtai = 0 - - shangjia_kuozhan.save() - - return Response({ - 'code': 0, - 'msg': '处罚申请提交成功', - 'data': { - 'dingdan_id': dingdan_id, - 'dashou_id': dashou_id, - 'chufa_liyou': chufa_liyou, - 'sqzhuangtai': 0, - 'jifen': 5 - } - }, 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({ - 'code': 10, - 'msg': '服务器内部错误,申请失败', - 'data': None - }, 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 - - - - - - - -'''class DashouDingdanHuoquView(APIView): - """ - 获取可抢订单列表 - POST /dingdan/ddhq - 需要JWT Token认证 - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取请求参数 - data = request.data - leixing_id = data.get('leixing_id') - page = int(data.get('page', 1)) - page_size = int(data.get('page_size', 10)) - - # 2. 构建基础查询条件 - base_query = Q(zhuangtai__in=[1, 7]) - if leixing_id: - base_query &= Q(leixing_id=leixing_id) - - # 3. 构建基础查询集(仅过滤,不执行) - base_qs = Dingdan.objects.filter(base_query).order_by('-create_time') - - # 4. 获取总记录数 - total = base_qs.count() - - # 5. 计算分页偏移量 - offset = (page - 1) * page_size - limit = page_size - - # 6. 获取当前页数据(数据库层切片,只返回当前页) - page_qs = base_qs[offset:offset + limit] - - # 7. 使用子查询获取商家昵称(性能优化,只针对当前页) - shangjia_sub = DingdanShangjia.objects.filter( - dingdan_id=OuterRef('id') - ).values('sjnicheng')[:1] - - # 8. 获取当前页所有需要的字段(包含商家昵称) - dingdan_values = page_qs.annotate( - sjnicheng=Subquery(shangjia_sub) - ).values( - 'dingdan_id', 'zhuangtai', 'fadan_pingtai', 'dashou_fencheng', - 'zhiding_id', 'leixing_id', 'tupian', 'yaoqiuleixing', - 'huiyuan_id', 'yongjin', 'jieshao', 'beizhu', 'create_time', - 'sjnicheng' - ) - - # 9. 转换为列表以便后续修改 - dingdan_list = list(dingdan_values) - - # 10. 批量获取指定打手的头像和昵称 - zhiding_ids = {item['zhiding_id'] for item in dingdan_list if item.get('zhiding_id')} - zhiding_info = {} - if zhiding_ids: - # 一次性查询所有打手信息(通过 UserMain 关联 UserDashou) - dashou_query = UserMain.objects.filter( - yonghuid__in=zhiding_ids - ).select_related('dashou_profile').annotate( - nicheng=F('dashou_profile__nicheng') - ).values('yonghuid', 'avatar', 'nicheng') - - zhiding_info = { - item['yonghuid']: { - 'avatar': item['avatar'] or '', - 'nicheng': item['nicheng'] or '' - } - for item in dashou_query - } - - # 11. 格式化返回数据,并添加打手头像和昵称 - formatted_list = [] - for order in dingdan_list: - zid = order.get('zhiding_id') - info = zhiding_info.get(zid, {}) - formatted_order = { - 'dingdan_id': order['dingdan_id'], - 'zhuangtai': order['zhuangtai'], - 'pingtai': order['fadan_pingtai'], - 'fadan_pingtai': order['fadan_pingtai'], - 'dashou_fencheng': float(order['dashou_fencheng']) if order['dashou_fencheng'] else 0.0, - 'zhiding_id': order['zhiding_id'] or '', - 'leixing_id': order['leixing_id'] or 0, - 'tupian': order['tupian'] or '', - 'yaoqiuleixing': order['yaoqiuleixing'] or 0, - 'huiyuan_id': order['huiyuan_id'] or '', - 'yajin': float(order['yongjin']) if order['yongjin'] else 0.0, - 'yongjin': float(order['yongjin']) if order['yongjin'] else 0.0, - 'jieshao': order['jieshao'] or '', - 'beizhu': order['beizhu'] or '', - 'creat_time': order['create_time'].strftime('%Y-%m-%d %H:%M:%S') if order['create_time'] else '', - 'sjnicheng': order['sjnicheng'] or '', - 'zhiding_uid': order['zhiding_id'] or '', - # 新增字段 - 'zhiding_avatar': info.get('avatar', ''), - 'zhiding_nicheng': info.get('nicheng', ''), - } - formatted_list.append(formatted_order) - - # 12. 判断是否有更多数据 - has_more = (page * page_size) < total - - return Response({ - 'code': 200, - 'msg': '获取成功', - 'data': { - 'list': formatted_list, - 'has_more': has_more, - 'total': total, - 'current_page': page, - 'page_size': page_size, - 'total_pages': (total + page_size - 1) // page_size, - } - })''' - - - - - - - - - -# 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): - """ - 获取可抢订单列表(支持标签筛选) - POST /dingdan/ddhq - 需要JWT Token认证 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取请求参数 - data = request.data - leixing_id = data.get('leixing_id') - page = int(data.get('page', 1)) - page_size = int(data.get('page_size', 10)) - biaoqian_id = data.get('biaoqian_id') - - # 2. 构建基础查询条件 - base_query = Q(zhuangtai__in=[1, 7]) - if leixing_id: - base_query &= Q(leixing_id=leixing_id) - - # 3. 标签筛选:先查出符合条件的订单ID,避免跨表 JOIN 排序冲突 - if biaoqian_id and int(biaoqian_id) > 0: - biaoqian_order_ids = DingdanBiaoqian.objects.filter( - chenghao_id=int(biaoqian_id) - ).values_list('dingdan_id', flat=True) - base_query &= Q(dingdan_id__in=list(biaoqian_order_ids)) - - # 4. 构建基础查询集 - base_qs = Dingdan.objects.filter(base_query).order_by('-create_time').distinct() - - # 5. 获取总记录数 - total = base_qs.count() - - # 6. 计算分页偏移量 - offset = (page - 1) * page_size - limit = page_size - - # 7. 获取当前页数据 - page_qs = base_qs[offset:offset + limit] - - # 8. 使用子查询获取商家昵称 - shangjia_sub = DingdanShangjia.objects.filter( - dingdan__dingdan_id=OuterRef('dingdan_id') - ).values('sjnicheng')[:1] - - # 9. 获取当前页所有需要的字段 - dingdan_values = page_qs.annotate( - sjnicheng=Subquery(shangjia_sub) - ).values( - 'dingdan_id', 'zhuangtai', 'fadan_pingtai', 'dashou_fencheng', - 'zhiding_id', 'leixing_id', 'tupian', 'yaoqiuleixing', - 'huiyuan_id', 'yongjin', 'jieshao', 'beizhu', 'create_time', - 'sjnicheng' - ) - - # 10. 转换为列表 - dingdan_list = list(dingdan_values) - - # 11. 批量获取指定打手的头像和昵称 - zhiding_ids = {item['zhiding_id'] for item in dingdan_list if item.get('zhiding_id')} - zhiding_info = {} - if zhiding_ids: - dashou_query = UserMain.objects.filter( - yonghuid__in=zhiding_ids - ).select_related('dashou_profile').annotate( - nicheng=F('dashou_profile__nicheng') - ).values('yonghuid', 'avatar', 'nicheng') - zhiding_info = { - item['yonghuid']: { - 'avatar': item['avatar'] or '', - 'nicheng': item['nicheng'] or '' - } - for item in dashou_query - } - - # ============ 批量获取三层标签 ============ - order_ids = [item['dingdan_id'] for item in dingdan_list] - - # --- 订单需求标签 --- - xuqiu_tags_map = {} - if order_ids: - try: - dbq_qs = DingdanBiaoqian.objects.filter( - dingdan_id__in=order_ids - ).select_related('chenghao') - for dbq in dbq_qs: - oid = dbq.dingdan_id - if oid not in xuqiu_tags_map: - xuqiu_tags_map[oid] = [] - ch = dbq.chenghao - xuqiu_tags_map[oid].append({ - 'id': ch.id, - 'mingcheng': ch.mingcheng, - 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu) - }) - except Exception as e: - logger.error(f"查询订单需求标签失败: {e}", exc_info=True) - - # --- 所有需要查询标签的用户ID集合(指定打手 + 商家) --- - yonghuid_set = set(zhiding_ids) - shangjia_order_ids = [item['dingdan_id'] for item in dingdan_list if item['fadan_pingtai'] == 2] - shangjia_id_map = {} - if shangjia_order_ids: - # 使用跨表查询 dingdan__dingdan_id__in 匹配字符串订单ID - sj_exts = DingdanShangjia.objects.filter( - dingdan__dingdan_id__in=shangjia_order_ids - ).values('dingdan_id', 'shangjia_id') - for sj in sj_exts: - shangjia_id_map[sj['dingdan_id']] = sj['shangjia_id'] - if sj['shangjia_id']: - yonghuid_set.add(sj['shangjia_id']) - - # 将 yonghuid 字符串转换为 UserMain 的主键 id - user_id_map = {} - if yonghuid_set: - users = UserMain.objects.filter(yonghuid__in=yonghuid_set).values('id', 'yonghuid') - for u in users: - user_id_map[u['yonghuid']] = u['id'] - - user_main_ids = list(user_id_map.values()) - user_tags_map = {} - if user_main_ids: - uc_qs = YonghuChenghao.objects.filter( - yonghu_id__in=user_main_ids - ).select_related('chenghao') - for uc in uc_qs: - uid = uc.yonghu_id - if uid not in user_tags_map: - user_tags_map[uid] = [] - user_tags_map[uid].append(uc.chenghao) - - dashou_tags_map = {} - shangjia_tags_map = {} - for item in dingdan_list: - oid = item['dingdan_id'] - # 指定打手标签 - zhid = item['zhiding_id'] - dashou_tags = [] - if zhid and zhid in user_id_map: - uid = user_id_map[zhid] - if uid in user_tags_map: - for ch in user_tags_map[uid]: - if ch.leixing == 'dashou': - dashou_tags.append({ - 'id': ch.id, - 'mingcheng': ch.mingcheng, - 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu) - }) - dashou_tags_map[oid] = dashou_tags - - # 商家标签 - sj_tags = [] - if item['fadan_pingtai'] == 2 and oid in shangjia_id_map: - sj_yonghuid = shangjia_id_map[oid] - if sj_yonghuid and sj_yonghuid in user_id_map: - uid = user_id_map[sj_yonghuid] - if uid in user_tags_map: - for ch in user_tags_map[uid]: - if ch.leixing == 'shangjia': - sj_tags.append({ - 'id': ch.id, - 'mingcheng': ch.mingcheng, - 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu) - }) - shangjia_tags_map[oid] = sj_tags - - # 12. 格式化返回数据 - formatted_list = [] - for order in dingdan_list: - oid = order['dingdan_id'] - zid = order.get('zhiding_id') - info = zhiding_info.get(zid, {}) - formatted_list.append({ - 'dingdan_id': order['dingdan_id'], - 'zhuangtai': order['zhuangtai'], - 'pingtai': order['fadan_pingtai'], - 'fadan_pingtai': order['fadan_pingtai'], - 'dashou_fencheng': float(order['dashou_fencheng']) if order['dashou_fencheng'] else 0.0, - 'zhiding_id': order['zhiding_id'] or '', - 'leixing_id': order['leixing_id'] or 0, - 'tupian': order['tupian'] or '', - 'yaoqiuleixing': order['yaoqiuleixing'] or 0, - 'huiyuan_id': order['huiyuan_id'] or '', - 'yajin': float(order['yongjin']) if order['yongjin'] else 0.0, - 'yongjin': float(order['yongjin']) if order['yongjin'] else 0.0, - 'jieshao': order['jieshao'] or '', - 'beizhu': order['beizhu'] or '', - 'creat_time': order['create_time'].strftime('%Y-%m-%d %H:%M:%S') if order['create_time'] else '', - 'sjnicheng': order['sjnicheng'] or '', - 'zhiding_uid': order['zhiding_id'] or '', - 'zhiding_avatar': info.get('avatar', ''), - 'zhiding_nicheng': info.get('nicheng', ''), - # 三层标签 - 'xuqiu_biaoqian': xuqiu_tags_map.get(oid, []), - 'dashou_biaoqian': dashou_tags_map.get(oid, []), - 'shangjia_biaoqian': shangjia_tags_map.get(oid, []), - }) - - has_more = (page * page_size) < total - - return Response({ - 'code': 200, - 'msg': '获取成功', - 'data': { - 'list': formatted_list, - 'has_more': has_more, - 'total': total, - 'current_page': page, - 'page_size': page_size, - 'total_pages': (total + page_size - 1) // page_size, - } - }) - - except Exception as e: - logger.error(f"订单列表查询失败: {e}", exc_info=True) - return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - def _parse_texiao_json(self, texiao_miaoshu): - if not texiao_miaoshu: - return {} - try: - return json.loads(texiao_miaoshu) - except: - return {} - - -# apps/dingdan/views.py -from shangpin.models import ShangpinLeixing # 假设这是你的商品类型模型 - - -class ShangpinLeixingHuoquView(APIView): - """ - 获取商品类型列表 - POST /dingdan/ddlxhq - 需要JWT Token认证 - """ - authentication_classes = [JWTAuthentication] - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 查询商品类型表 - # 假设你的商品类型模型字段为:id, jieshao, tupian_url - leixing_list = ShangpinLeixing.objects.all().values( - 'id', # 商品类型ID - 'jieshao', # 类型介绍 - 'tupian_url' # 类型图片URL(相对URL) - ).order_by('id') # 按ID排序,可根据需要调整 - - # 2. 格式化返回数据 - formatted_list = [] - for leixing in leixing_list: - formatted_leixing = { - 'id': leixing['id'], - 'jieshao': leixing['jieshao'] or '', # 处理空值 - 'tupian_url': leixing['tupian_url'] or '' # 处理空值 - } - formatted_list.append(formatted_leixing) - - # 3. 返回响应 - return Response({ - 'code': 200, - 'msg': '获取成功', - 'data': { - 'list': formatted_list, - 'total': len(formatted_list) - } - }) - - -# 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__) - - - - - - -'''class QiangdanView(APIView): - authentication_classes = [JWTAuthentication] - permission_classes = [IsAuthenticated] - - def post(self, request): - data = request.data - dingdan_id = data.get('dingdan_id') - if not dingdan_id: - return Response({'code': 400, 'msg': '订单ID不能为空'}, status=400) - - try: - with transaction.atomic(): - # 1. 验证打手身份和状态 - dashou_profile = self._validate_dashou_status(request.user) - if not dashou_profile: - return Response({'code': 403, 'msg': '您不是打手或账号状态异常'}, status=403) - - # 2. 锁定并验证订单(行锁) - order_result = self._validate_and_lock_order(dingdan_id, request.user.yonghuid) - if isinstance(order_result, str): - return Response({'code': 400, 'msg': order_result}) - order = order_result - - is_cross = order.is_cross == 1 - dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单 - - # ========== 跨平台订单处理 ========== - if is_cross and dispatch_type == 2: - # 对方派单(跨平台且对方派给我方) - validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) - if not validation_result['success']: - return Response({'code': 400, 'msg': validation_result['message']}) - - # 向对方平台发送抢单询问 - claim_allowed = self._request_partner_check_claim(order, request.user.yonghuid) - if claim_allowed is None: - return Response({'code': 500, 'msg': '跨平台通信失败,请重试'}, status=500) - elif claim_allowed is False: - if order.zhuangtai == 1: - order.zhuangtai = 21 - order.save() - logger.info(f"订单 {dingdan_id} 被对方拒绝抢单,状态改为21") - else: - order.zhuangtai = 20 - order.save() - logger.info(f"订单 {dingdan_id} 已被其他平台抢走,状态改为20") - return Response({'code': 400, 'msg': '订单已被其他平台打手抢走或不可抢'}) - - # 允许抢单,执行本地抢单 - self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) - - # 更新抢单统计(对方派单,我方抢到) - try: - update_daily_dispatch_stat( - direction=2, # 对方派单 - partner_club_id=order.partner_club_id or '000001', - claimed_amount=order.jine, # 订单金额 - claimed_count=1 - ) - except Exception as e: - logger.error(f"更新抢单统计失败: {e}", exc_info=True) - - elif is_cross and dispatch_type == 1: - # 我方派单 - validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) - if not validation_result['success']: - return Response({'code': 400, 'msg': validation_result['message']}) - - self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) - - # 异步通知所有合作俱乐部:此订单已被抢 - self._async_notify_partners_order_claimed(order) - - else: - # 普通订单 - validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) - if not validation_result['success']: - return Response({'code': 400, 'msg': validation_result['message']}) - self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) - - # 抢单成功后建立聊天关系(事务外) - chat_success = self._establish_chat_relationship(order, request.user.yonghuid, dashou_profile, request.user.avatar) - - # 🔥 新增:调用打手每日统计(成交) - try: - update_dashou_daily_by_action( - yonghuid=order.jiedan_dashou_id, - amount=order.dashou_fencheng, - action=1 # 1 表示接单(接单) - ) - logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - # 不影响主流程 - - return Response({ - 'code': 200, - 'msg': '抢单成功', - 'data': { - 'dingdan_id': dingdan_id, - 'chat_established': chat_success - } - }) - - except Exception as e: - logger.error(f"抢单异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '系统异常,请稍后重试'}, status=500) - - # ---------- 辅助方法 ---------- - def _validate_dashou_status(self, user): - try: - dashou_profile = user.dashou_profile - if (dashou_profile.zhuangtai != 1 or - dashou_profile.zhanghaozhuangtai != 1 or - dashou_profile.jifen < 5): - return None - return dashou_profile - except UserDashou.DoesNotExist: - return None - - def _validate_and_lock_order(self, dingdan_id, dashou_yonghuid): - try: - order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) - if order.zhuangtai not in [1, 7]: - return "订单状态不可抢" - if order.zhuangtai == 7: - if not order.zhiding_id or order.zhiding_id != dashou_yonghuid: - return "此订单为指定单,您不是指定打手" - if order.jiedan_dashou_id: - return "订单已被其他打手抢走" - return order - except Dingdan.DoesNotExist: - return "订单不存在" - - def _validate_order_requirements(self, order, dashou_profile, dashou_yonghuid): - if order.yaoqiuleixing == 1: - return self._validate_huiyuan_requirement(order, dashou_yonghuid) - elif order.yaoqiuleixing == 2: - return self._validate_yajin_requirement(order, dashou_profile) - return {'success': True, 'message': '验证通过'} - - def _validate_huiyuan_requirement(self, order, dashou_yonghuid): - try: - huiyuan_record = Huiyuangoumai.objects.get( - huiyuan_id=order.huiyuan_id, - yonghu_id=dashou_yonghuid - ) - if huiyuan_record.jiance_shifou_daoqi() or huiyuan_record.huiyuan_zhuangtai != 1: - return {'success': False, 'message': '会员已过期或未开通对应会员'} - return {'success': True, 'message': '会员验证通过'} - except Huiyuangoumai.DoesNotExist: - return {'success': False, 'message': '未开通对应会员,无法抢此订单'} - - def _validate_yajin_requirement(self, order, dashou_profile): - yajin_required = order.yongjin or 0 - dashou_yajin = dashou_profile.yajin or 0 - if dashou_yajin < yajin_required: - return {'success': False, 'message': f'押金不足,需要{yajin_required}元,当前有{dashou_yajin}元'} - return {'success': True, 'message': '押金验证通过'} - - def _execute_qiangdan(self, order, dashou_profile, dashou_yonghuid): - order.zhuangtai = 2 - order.jiedan_dashou_id = dashou_yonghuid - order.user2_id = f"Ds{dashou_yonghuid}" - order.save() - - dashou_profile.zhuangtai = 0 - dashou_profile.jiedanzongliang = F('jiedanzongliang') + 1 - dashou_profile.jinrijiedan = F('jinrijiedan') + 1 - dashou_profile.jinyuejiedan = F('jinyuejiedan') + 1 - dashou_profile.save() - - # ---------- 跨平台询问对方 ---------- - def _request_partner_check_claim(self, order, dashou_id): - partner_club_id = order.partner_club_id or '000001' - try: - club = Club.objects.filter(partner_club_id=partner_club_id).first() - if not club or not club.partner_domain: - logger.error(f"未找到对方俱乐部配置: partner_club_id={partner_club_id}") - return None - - my_club = Club.objects.filter(partner_club_id=club.partner_club_id).first() - if not my_club: - logger.error(f"未找到我方俱乐部配置: partner_club_id={club.partner_club_id}") - return None - - payload = { - 'token': settings.CROSS_PLATFORM_TOKEN, - 'wofangdingdan_id': order.partner_order_id, - 'partner_order_id': order.dingdan_id, - 'our_club_id': my_club.club_id, - 'partner_club_id': club.partner_club_id, - 'dashou_id': dashou_id, - 'sj_id': f"Sj{settings.AGENT_SHANGJIA_ID}" - } - - url = club.partner_domain.rstrip('/') + '/dingdan/partner_check_claim' - resp = requests.post(url, json=payload, timeout=5) - - if resp.status_code != 200: - logger.error(f"跨平台抢单询问HTTP失败: {resp.status_code}") - return None - - data = resp.json() - if data.get('code') == 0: - return data.get('data', {}).get('can_claim') is True - else: - logger.error(f"跨平台抢单询问业务失败: {data}") - return False - except Exception as e: - logger.error(f"跨平台抢单询问异常: {e}", exc_info=True) - return None - - # ---------- 异步通知其他俱乐部 ---------- - def _async_notify_partners_order_claimed(self, order): - def notify_task(): - clubs = Club.objects.filter(is_interop_enabled=True).select_related() - for club in clubs: - for attempt in range(1, 4): - success = self._send_claim_notification_to_partner(order, club) - if success: - break - if attempt < 3: - import time - time.sleep(2 ** attempt) - else: - logger.error(f"通知俱乐部 {club.club_id} 订单已抢失败,重试3次后仍失败") - - thread = threading.Thread(target=notify_task) - thread.daemon = True - thread.start() - - def _send_claim_notification_to_partner(self, order, club): - try: - my_club = Club.objects.filter(partner_club_id=club.partner_club_id).first() - if not my_club: - logger.warning(f"未找到我方俱乐部配置: partner_club_id={club.partner_club_id}") - return False - - payload = { - 'token': settings.CROSS_PLATFORM_TOKEN, - 'our_club_id': my_club.club_id, - 'partner_club_id': club.partner_club_id, - 'partner_order_id': order.dingdan_id, - 'claimed': True - } - url = club.partner_domain.rstrip('/') + '/dingdan/partner_notify_claim' - resp = requests.post(url, json=payload, timeout=3) - return resp.status_code == 200 - except Exception as e: - logger.error(f"通知俱乐部 {club.club_id} 异常: {e}") - return False - - # ---------- 聊天关系建立(完整版) ---------- - def _establish_chat_relationship(self, order, dashou_yonghuid, dashou_profile, dashou_avatar): - try: - dashou_info = { - 'goeasy_id': f"Ds{dashou_yonghuid}", - 'avatar': dashou_avatar or '', - 'nickname': dashou_profile.nicheng or f'打手{dashou_yonghuid}' - } - - laoban_info = self._get_laoban_info(order) - if not laoban_info: - return False - - # 打手给老板/商家发消息 - success1 = self._send_goeasy_message( - sender_id=dashou_info['goeasy_id'], - sender_data={ - "avatar": dashou_info['avatar'], - "name": dashou_info['nickname'], - "nickname": dashou_info['nickname'] - }, - receiver_id=laoban_info['goeasy_id'], - receiver_data={ - "avatar": laoban_info['avatar'], - "name": laoban_info['nickname'], - "nickname": laoban_info['nickname'] - }, - message_type="text", - message_payload={ - "action": "new_chat", - "partner_id": laoban_info['goeasy_id'], - "partner_info": { - "userId": laoban_info['goeasy_id'], - "avatar": laoban_info['avatar'], - "name": laoban_info['nickname'], - "nickname": laoban_info['nickname'] - }, - "order_id": order.dingdan_id, - "message": f"您好,我已接单:{order.jieshao},您的备注是:{order.beizhu},游戏ID是:{order.nicheng} 请确认订单信息" - } - ) - - # 老板/商家给打手发消息 - success2 = self._send_goeasy_message( - sender_id=laoban_info['goeasy_id'], - sender_data={ - "avatar": laoban_info['avatar'], - "name": laoban_info['nickname'], - "nickname": laoban_info['nickname'] - }, - receiver_id=dashou_info['goeasy_id'], - receiver_data={ - "avatar": dashou_info['avatar'], - "name": dashou_info['nickname'], - "nickname": dashou_info['nickname'] - }, - message_type="text", - message_payload={ - "action": "new_chat", - "partner_id": dashou_info['goeasy_id'], - "partner_info": { - "userId": dashou_info['goeasy_id'], - "avatar": dashou_info['avatar'], - "name": dashou_info['nickname'], - "nickname": dashou_info['nickname'] - }, - "order_id": order.dingdan_id, - "message": f"好的,订单已确认,请开始服务" - } - ) - - return success1 and success2 - - except Exception as e: - logger.error(f"建立聊天关系异常: {e}", exc_info=True) - return False - - def _get_laoban_info(self, order): - try: - laoban_info = { - 'goeasy_id': '', - 'avatar': '', - 'nickname': '' - } - - if order.fadan_pingtai == 1: - pingtai_kuozhan = DingdanPingtai.objects.filter(dingdan=order).first() - if not pingtai_kuozhan or not pingtai_kuozhan.laoban_id: - return None - yonghuid = pingtai_kuozhan.laoban_id - laoban_info['goeasy_id'] = f"Boss{yonghuid}" - laoban_user = UserMain.objects.filter(yonghuid=yonghuid).first() - if not laoban_user: - return None - laoban_info['avatar'] = laoban_user.avatar or '' - boss_profile = UserBoss.objects.filter(user=laoban_user).first() - laoban_info['nickname'] = boss_profile.nickname if boss_profile and boss_profile.nickname else f'老板{yonghuid}' - - elif order.fadan_pingtai == 2: - shangjia_kuozhan = DingdanShangjia.objects.filter(dingdan=order).first() - if not shangjia_kuozhan or not shangjia_kuozhan.shangjia_id: - return None - yonghuid = shangjia_kuozhan.shangjia_id - laoban_info['goeasy_id'] = f"Sj{yonghuid}" - shangjia_user = UserMain.objects.filter(yonghuid=yonghuid).first() - if not shangjia_user: - return None - laoban_info['avatar'] = shangjia_user.avatar or '' - if shangjia_kuozhan.sjnicheng: - laoban_info['nickname'] = shangjia_kuozhan.sjnicheng - else: - shop_profile = UserShangjia.objects.filter(user=shangjia_user).first() - laoban_info['nickname'] = shop_profile.nicheng if shop_profile else f'商家{yonghuid}' - else: - if order.user1_id: - laoban_info['goeasy_id'] = order.user1_id - if laoban_info['goeasy_id'].startswith('Boss'): - yonghuid = laoban_info['goeasy_id'][4:] - laoban_info['nickname'] = f'老板{yonghuid}' - elif laoban_info['goeasy_id'].startswith('Sj'): - yonghuid = laoban_info['goeasy_id'][2:] - laoban_info['nickname'] = f'商家{yonghuid}' - else: - laoban_info['nickname'] = '用户' - else: - return None - - if not laoban_info['goeasy_id']: - return None - return laoban_info - - except Exception as e: - logger.error(f"获取老板信息异常: {e}", exc_info=True) - return None - - def _send_goeasy_message(self, sender_id, sender_data, receiver_id, receiver_data, message_type, message_payload): - try: - if not hasattr(settings, 'GOEASY_APPKEY') or not settings.GOEASY_APPKEY: - return False - if not hasattr(settings, 'GOEASY_SECRET') or not settings.GOEASY_SECRET: - return False - if not hasattr(settings, 'GOEASY_REST_URL') or not settings.GOEASY_REST_URL: - return False - - appkey = settings.GOEASY_APPKEY - secret = settings.GOEASY_SECRET - url = settings.GOEASY_REST_URL - - if isinstance(message_payload, dict) and 'message' in message_payload: - payload_text = str(message_payload['message']) - else: - payload_text = str(message_payload) - - payload_data = {"text": payload_text} - payload_str = json.dumps(payload_data, ensure_ascii=False) - - request_data = { - "appkey": appkey, - "senderId": str(sender_id), - "senderData": { - "avatar": str(sender_data.get('avatar', '')), - "name": str(sender_data.get('name', '系统通知')) - }, - "to": { - "type": "private", - "id": str(receiver_id), - "data": { - "avatar": str(receiver_data.get('avatar', '')), - "name": str(receiver_data.get('name', '用户')) - } - }, - "type": "text", - "payload": payload_str - } - - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {secret}" - } - - response = requests.post(url, headers=headers, json=request_data, timeout=10) - return response.status_code == 200 - - except Exception as e: - logger.error(f"发送GoEasy消息异常: {e}", exc_info=True) - return False''' - - - - - -# 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') - - -# ----------------- 基础配置获取 ----------------- -def get_self_goeasy_appkey(): - try: - cfg = ClubConfig.objects.filter(is_self=1).first() - return cfg.chat_api_key if cfg else None - except Exception: - return None - - -def get_self_goeasy_secret(): - try: - cfg = ClubConfig.objects.filter(is_self=1).first() - return cfg.chat_api_id if cfg else None - except Exception: - return None - - -def get_self_storage_domain(): - try: - cfg = ClubConfig.objects.filter(is_self=1).first() - return cfg.storage_bucket_domain if cfg else '' - except Exception: - return '' - - -def full_avatar(relative_url, bucket_domain=None): - if not relative_url: - return '' - if relative_url.startswith('http'): - return relative_url - domain = bucket_domain or get_self_storage_domain() - if domain: - domain = domain.rstrip('/') + '/' - return domain + relative_url.lstrip('/') - return relative_url - - -# ----------------- GoEasy API 封装 ----------------- -def subscribe_users_to_group(user_ids, group_ids, appkey=None, secret=None): - """订阅用户到群组,确保能收发该群消息""" - if not appkey: - appkey = get_self_goeasy_appkey() - if not secret: - secret = get_self_goeasy_secret() - if not appkey: - logger.error("订阅失败:GoEasy AppKey 未配置") - return False - url = 'https://rest-hangzhou.goeasy.io/v2/im/subscribe-groups' - body = {"appkey": appkey, "userIds": user_ids, "groupIds": group_ids} - headers = {"Content-Type": "application/json"} - if secret: - headers["Authorization"] = f"Bearer {secret}" - try: - r = requests.post(url, headers=headers, json=body, timeout=10) - if r.status_code == 200: - logger.info(f"订阅成功:用户 {user_ids} -> 群 {group_ids}") - return True - logger.error(f"订阅失败:{r.status_code} {r.text}") - return False - except Exception as e: - logger.exception("订阅异常") - return False - - -def send_group_message( - appkey, secret, group_id, sender_id, sender_name, sender_avatar, - message_text='', custom_payload=None, group_name='', order_id='', is_cross=0 -): - """ - 通过 GoEasy REST API 发送群消息。 - message_text: 普通文本内容 - custom_payload: 自定义消息体(如订单卡片),此时 type 设为 'order' - """ - url = 'https://rest-hangzhou.goeasy.io/v2/im/message' - to_data = { - "name": group_name or group_id, - "avatar": sender_avatar or '', - "orderId": order_id, - "isCross": is_cross - } - msg_type = 'order' if custom_payload else 'text' - payload = custom_payload if custom_payload else message_text - - body = { - "appkey": appkey, - "senderId": sender_id, - "senderData": {"avatar": sender_avatar, "name": sender_name}, - "to": {"type": "group", "id": group_id, "data": to_data}, - "type": msg_type, - "payload": payload - } - headers = {"Content-Type": "application/json"} - if secret: - headers["Authorization"] = f"Bearer {secret}" - - for attempt in range(3): # 最多重试3次 - try: - r = requests.post(url, headers=headers, json=body, timeout=10) - if r.status_code == 200: - logger.info(f"消息发送成功:sender={sender_id}, group={group_id}, type={msg_type}") - return True - logger.warning(f"消息发送失败 (尝试 {attempt+1}/3):状态码 {r.status_code}, 响应 {r.text}") - except Exception as e: - logger.warning(f"消息发送异常 (尝试 {attempt+1}/3):{e}") - if attempt < 2: - time.sleep(1) - logger.error(f"消息最终发送失败:sender={sender_id}, group={group_id}") - return False - - -# ----------------- 权限校验 ----------------- -def check_user_permission(identity_type, uid, order): - """验证用户身份、账号状态、以及是否属于该订单""" - try: - user = UserMain.objects.get(yonghuid=uid) - except UserMain.DoesNotExist: - return False, '用户不存在' - - if identity_type == 'dashou': - try: - dashou = UserDashou.objects.get(user=user) - if dashou.zhanghaozhuangtai != 1 : - return False, '打手账号已被封禁或状态异常' - except UserDashou.DoesNotExist: - return False, '该用户不是打手' - if order.jiedan_dashou_id != uid: - return False, '你不是该订单的接单打手' - - elif identity_type == 'shangjia': - try: - shop = UserShangjia.objects.get(user=user) - if shop.zhuangtai != 1: - return False, '商家账号已被禁用' - except UserShangjia.DoesNotExist: - return False, '该用户不是商家' - try: - if order.shangjia_kuozhan.shangjia_id != uid: - return False, '你不是该订单的发布商家' - except DingdanShangjia.DoesNotExist: - return False, '订单缺少商家信息' - - elif identity_type == 'boss': - try: - UserBoss.objects.get(user=user) # 老板暂无封禁字段,仅需存在 - except UserBoss.DoesNotExist: - return False, '该用户不是老板' - try: - if order.pingtai_kuozhan.laoban_id != uid: - return False, '你不是该订单的老板' - except DingdanPingtai.DoesNotExist: - return False, '订单缺少平台信息' - - else: - return False, '不支持的身份类型' - return True, '' - - -# ----------------- 跨平台对方配置获取 ----------------- -def get_partner_club_config(partner_club_id): - try: - return ClubConfig.objects.get(club_id=partner_club_id, is_self=0) - except ClubConfig.DoesNotExist: - return None - - -# ----------------- 主视图 ----------------- - - - -class DaiLiQunLiaoXiaoXiView(APIView): - """ - 代理发送群聊消息 - 支持: - - 非跨平台订单:直接用我方 GoEasy Key 以真实身份发送 - - 跨平台订单(双方任意一方派单):除了我方群外,还需用对方 GoEasy Key 以代理身份向对方群发送 - 跨平台判断条件:订单同时存在 partner_order_id 和 partner_club_id 即视为跨平台 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - # ===== 1. 参数解析 ===== - order_id = request.data.get('orderId') - identity_type = request.data.get('identityType') # dashou / shangjia / boss - sender_name = request.data.get('senderName', '') - sender_avatar_raw = request.data.get('senderAvatar', '') - message_type = request.data.get('messageType', 'text') - message_text = request.data.get('text', '') - order_payload = request.data.get('orderPayload', None) - - logger.info(f"[群聊代理] 收到请求: orderId={order_id}, identityType={identity_type}, " - f"messageType={message_type}, text={message_text[:20]}...") - - if not order_id or not identity_type: - return Response({'code': 400, 'msg': '缺少 orderId 或 identityType'}) - if identity_type not in ('dashou', 'shangjia', 'boss'): - return Response({'code': 400, 'msg': 'identityType 无效'}) - - # ===== 2. 构造 senderId ===== - uid = request.user.yonghuid - prefix_map = {'dashou': 'Ds', 'shangjia': 'Sj', 'boss': 'Boss'} - sender_id = prefix_map[identity_type] + uid - logger.info(f"[群聊代理] 登录用户 yonghuid={uid}, 构造 senderId={sender_id}") - - # ===== 3. 查询订单 ===== - try: - order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=order_id) - except Dingdan.DoesNotExist: - logger.warning(f"[群聊代理] 订单 {order_id} 不存在") - return Response({'code': 404, 'msg': '订单不存在'}) - - # ===== 4. 权限校验 ===== - valid, err_msg = check_user_permission(identity_type, uid, order) - if not valid: - logger.warning(f"[群聊代理] 权限校验失败: {err_msg}, uid={uid}, identity={identity_type}") - return Response({'code': 403, 'msg': err_msg}) - - # ===== 5. 我方 GoEasy 配置 ===== - self_appkey = get_self_goeasy_appkey() - self_secret = get_self_goeasy_secret() - if not self_appkey: - logger.error("[群聊代理] 我方 GoEasy AppKey 未配置") - return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) - - # ===== 6. 公共群信息 ===== - real_group_id = f"group_{order.dingdan_id}" - group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else f"订单{order.dingdan_id}" - sender_avatar = full_avatar(sender_avatar_raw) if sender_avatar_raw else full_avatar('') - - # ===== 7. 订阅发送者到群 ===== - if not subscribe_users_to_group([sender_id], [real_group_id], self_appkey, self_secret): - logger.error("[群聊代理] 订阅群组失败") - return Response({'code': 500, 'msg': '订阅群组失败'}) - - # ===== 8. 判断跨平台(只看对方订单ID和俱乐部ID) ===== - partner_order_id = order.partner_order_id - partner_club_id = order.partner_club_id - is_cross = 1 if (partner_order_id and partner_club_id) else 0 - logger.info(f"[群聊代理] 订单 {order_id} partner_order_id={partner_order_id}, partner_club_id={partner_club_id}, 判定跨平台={is_cross}") - - custom_payload = order_payload if message_type == 'order' else None - text_content = message_text - - # ---------- 先向我方群发送消息(永远执行) ---------- - local_ok = send_group_message( - appkey=self_appkey, - secret=self_secret, - group_id=real_group_id, - sender_id=sender_id, - sender_name=sender_name, - sender_avatar=sender_avatar, - message_text=text_content, - custom_payload=custom_payload, - group_name=group_name, - order_id=order.dingdan_id, - is_cross=is_cross - ) - if not local_ok: - return Response({'code': 500, 'msg': '消息发送失败'}) - - # ---------- 跨平台转发(如果有对方订单ID和俱乐部ID) ---------- - if is_cross: - logger.info("[群聊代理] 满足跨平台条件,准备双平台发送") - partner_cfg = get_partner_club_config(partner_club_id) - if not partner_cfg or not partner_cfg.chat_api_key: - logger.warning("[群聊代理] 对方俱乐部配置不可用,仅发送我方群") - return Response({'code': 200, 'msg': '发送成功(对方GoEasy未配置)'}) - - partner_appkey = partner_cfg.chat_api_key - partner_secret = partner_cfg.chat_api_id or '' - partner_group_id = f"group_{partner_order_id}" - - # 构造代理身份 - agent_sender_id = f"{order.dingdan_id}_{sender_id}" - if identity_type == 'dashou': - agent_sender_name = f"[我方]{sender_name}" - elif identity_type == 'shangjia': - agent_sender_name = f"[我方商家]{sender_name}" - else: - agent_sender_name = f"[我方老板]{sender_name}" - agent_avatar = full_avatar('') - - logger.info(f"[群聊代理] 对方群组={partner_group_id}, 代理身份={agent_sender_id}/{agent_sender_name}") - - # 向对方群发送代理消息 - partner_ok = send_group_message( - appkey=partner_appkey, - secret=partner_secret, - group_id=partner_group_id, - sender_id=agent_sender_id, - sender_name=agent_sender_name, - sender_avatar=agent_avatar, - message_text=text_content, - custom_payload=custom_payload, - group_name='跨平台订单交流', - order_id=partner_order_id, - is_cross=1 - ) - logger.info(f"[群聊代理] 对方群发送结果: {partner_ok}") - - if partner_ok: - return Response({'code': 200, 'msg': '发送成功(双平台)'}) - else: - return Response({'code': 200, 'msg': '我方发送成功,对方发送失败'}) - else: - logger.info("[群聊代理] 非跨平台,仅发送我方群") - return Response({'code': 200, 'msg': '发送成功'}) - - -#通过跨平台字段判断是否是跨平台 -'''class DaiLiQunLiaoXiaoXiView(APIView): - """ - 代理发送群聊消息 - - 永远向我方群 group_{订单ID} 发送真实身份消息 - - 如果是跨平台订单且存在 partner_order_id + partner_club_id,且对方配置有效, - 则同时向对方群 group_{partner_order_id} 发送代理消息(不管派单方向) - - 前端传递 identityType: 'dashou' / 'shangjia' / 'boss' - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - # ===== 1. 参数解析 ===== - order_id = request.data.get('orderId') - identity_type = request.data.get('identityType') # 原有字段:dashou / shangjia / boss - sender_name = request.data.get('senderName', '') - sender_avatar_raw = request.data.get('senderAvatar', '') - message_type = request.data.get('messageType', 'text') - message_text = request.data.get('text', '') - order_payload = request.data.get('orderPayload', None) - - logger.info(f"[群聊代理] orderId={order_id}, identityType={identity_type}, text={message_text[:20]}...") - - if not order_id or not identity_type: - return Response({'code': 400, 'msg': '缺少 orderId 或 identityType'}) - if identity_type not in ('dashou', 'shangjia', 'boss'): - return Response({'code': 400, 'msg': 'identityType 无效 (dashou/shangjia/boss)'}) - - # ===== 2. 构造 senderId ===== - uid = request.user.yonghuid - prefix_map = {'dashou': 'Ds', 'shangjia': 'Sj', 'boss': 'Boss'} - sender_id = prefix_map[identity_type] + uid - # 记录真实身份名称(用于代理命名) - identity_name_map = {'dashou': '打手', 'shangjia': '商家', 'boss': '老板'} - identity_name = identity_name_map[identity_type] - logger.info(f"[群聊代理] 用户 yonghuid={uid}, senderId={sender_id}, 身份={identity_name}") - - # ===== 3. 查询订单 ===== - try: - order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=order_id) - except Dingdan.DoesNotExist: - logger.warning(f"[群聊代理] 订单 {order_id} 不存在") - return Response({'code': 404, 'msg': '订单不存在'}) - - # ===== 4. 权限校验 ===== - valid, err_msg = check_user_permission(identity_type, uid, order) - if not valid: - logger.warning(f"[群聊代理] 权限校验失败: {err_msg}, uid={uid}, role={identity_type}") - return Response({'code': 403, 'msg': err_msg}) - - # ===== 5. 我方 GoEasy 配置 ===== - self_appkey = get_self_goeasy_appkey() - self_secret = get_self_goeasy_secret() - if not self_appkey: - logger.error("[群聊代理] 我方 GoEasy AppKey 未配置") - return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) - - # ===== 6. 公共群信息(我方) ===== - real_group_id = f"group_{order.dingdan_id}" - group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else f"订单{order.dingdan_id}" - sender_avatar = full_avatar(sender_avatar_raw) if sender_avatar_raw else full_avatar('') - is_cross = order.is_cross - - # ===== 7. 先发送到我方群(永远执行) ===== - local_ok = send_group_message( - appkey=self_appkey, - secret=self_secret, - group_id=real_group_id, - sender_id=sender_id, - sender_name=sender_name, - sender_avatar=sender_avatar, - message_text=message_text, - custom_payload=order_payload if message_type == 'order' else None, - group_name=group_name, - order_id=order.dingdan_id, - is_cross=1 if is_cross == 1 else 0 - ) - logger.info(f"[群聊代理] 我方群发送结果: {local_ok}") - if not local_ok: - return Response({'code': 500, 'msg': '消息发送失败'}) - - # ===== 8. 判断是否需要向对方群发代理消息 ===== - partner_club_id = order.partner_club_id - partner_order_id = order.partner_order_id - - # 非跨平台,或者缺少对方订单/俱乐部ID,直接结束 - if is_cross != 1 or not partner_club_id or not partner_order_id: - logger.info("[群聊代理] 未满足跨平台双发条件,仅发送我方群") - return Response({'code': 200, 'msg': '发送成功'}) - - # 获取对方俱乐部配置 - partner_cfg = get_partner_club_config(partner_club_id) - if not partner_cfg or not partner_cfg.chat_api_key: - logger.warning(f"[群聊代理] 对方俱乐部配置不可用,仅发送我方群") - return Response({'code': 200, 'msg': '发送成功(对方GoEasy未配置)'}) - - # ===== 9. 构造代理身份并发送到对方群 ===== - agent_sender_id = f"{order.dingdan_id}_{sender_id}" # 代理ID - agent_sender_name = f"[我方{identity_name}]{uid}" # 代理名称:如 [我方打手]1234567 - agent_avatar = full_avatar('') - - partner_group_id = f"group_{partner_order_id}" - logger.info(f"[群聊代理] 对方群组={partner_group_id}, 代理身份={agent_sender_id}/{agent_sender_name}") - - partner_ok = send_group_message( - appkey=partner_cfg.chat_api_key, - secret=partner_cfg.chat_api_id or '', - group_id=partner_group_id, - sender_id=agent_sender_id, - sender_name=agent_sender_name, - sender_avatar=agent_avatar, - message_text=message_text, - custom_payload=order_payload if message_type == 'order' else None, - group_name='跨平台订单交流', - order_id=partner_order_id, - is_cross=1 - ) - logger.info(f"[群聊代理] 对方群发送结果: {partner_ok}") - - if partner_ok: - return Response({'code': 200, 'msg': '发送成功(双平台)'}) - else: - return Response({'code': 200, 'msg': '我方发送成功,对方发送失败'})''' - - - - - - - - - - - - -from peizhi.models import ClubConfig, ShangjiaLianjie -logger = logging.getLogger('dingdan') - -# ---------- 基础配置 ---------- -def get_self_goeasy_appkey(): - try: - cfg = ClubConfig.objects.filter(is_self=1).first() - return cfg.chat_api_key if cfg else None - except Exception: - return None - -def get_self_goeasy_secret(): - try: - cfg = ClubConfig.objects.filter(is_self=1).first() - return cfg.chat_api_id if cfg else None - except Exception: - return None - -def get_partner_club_config(partner_club_id): - try: - return ClubConfig.objects.get(club_id=partner_club_id, is_self=0) - except ClubConfig.DoesNotExist: - return None - - -class ShangjiaLianjieLiuYanZhuanFaView(APIView): - """ - 商家链接留言转发接口 - 路径:POST /dingdan/sjljlbxxzf - 固定以 B{商家ID} 身份发送群消息。通过 partner_order_id/partner_club_id 判断是否跨平台。 - """ - permission_classes = [] - - def post(self, request): - token = request.data.get('token') - text = request.data.get('text', '') - - if not token or not text: - return Response({'code': 400, 'msg': '缺少参数'}) - - try: - lianjie = ShangjiaLianjie.objects.get(lianjie_token=token) - except ShangjiaLianjie.DoesNotExist: - return Response({'code': 404, 'msg': '链接不存在或已失效'}) - - if not lianjie.dingdan_id: - return Response({'code': 400, 'msg': '链接缺少订单ID'}) - - try: - order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=lianjie.dingdan_id) - except Dingdan.DoesNotExist: - return Response({'code': 500, 'msg': '订单数据异常'}) - - shangjia_uid = lianjie.yonghu_id - sender_id = f"B{shangjia_uid}" - sender_name = "老板" - sender_avatar = '' - - appkey = get_self_goeasy_appkey() - secret = get_self_goeasy_secret() - if not appkey: - return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) - - group_id = f"group_{order.dingdan_id}" - group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else f"订单{order.dingdan_id}" - - local_cross_flag = 1 if (order.partner_order_id and order.partner_club_id) else 0 - - if not _subscribe_users_to_group([sender_id], [group_id], appkey, secret): - return Response({'code': 500, 'msg': '订阅群组失败'}) - - local_ok = _send_group_message( - appkey=appkey, - secret=secret, - group_id=group_id, - sender_id=sender_id, - sender_name=sender_name, - sender_avatar=sender_avatar, - message_text=text, - custom_payload=None, - group_name=group_name, - order_id=order.dingdan_id, - is_cross=local_cross_flag - ) - if not local_ok: - return Response({'code': 500, 'msg': '消息发送失败'}) - - # 跨平台转发 - partner_order_id = order.partner_order_id - partner_club_id = order.partner_club_id - - if not partner_order_id or not partner_club_id: - return Response({'code': 200, 'msg': '发送成功'}) - - partner_cfg = get_partner_club_config(partner_club_id) - if not partner_cfg or not partner_cfg.chat_api_key: - return Response({'code': 200, 'msg': '发送成功(对方未配置)'}) - - agent_sender_id = f"{order.dingdan_id}_{sender_id}" - agent_sender_name = f"[我方老板]{shangjia_uid}" - - partner_group_id = f"group_{partner_order_id}" - - partner_ok = _send_group_message( - appkey=partner_cfg.chat_api_key, - secret=partner_cfg.chat_api_id or '', - group_id=partner_group_id, - sender_id=agent_sender_id, - sender_name=agent_sender_name, - sender_avatar='', - message_text=text, - custom_payload=None, - group_name='跨平台订单交流', - order_id=partner_order_id, - is_cross=1 - ) - - if partner_ok: - return Response({'code': 200, 'msg': '发送成功(双平台)'}) - else: - return Response({'code': 200, 'msg': '我方发送成功,对方发送失败'}) - - - - - - - - - - - - - -'''class DaiLiQunLiaoXiaoXiView(APIView): - """ - 代理发送群聊消息 - 支持: - - 非跨平台订单:直接用我方 GoEasy Key 以真实身份发送 - - 跨平台订单(我方派单):同上 - - 跨平台订单(对方派单):除了我方群外,还需用对方 GoEasy Key 以代理身份向对方群发送 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - # ===== 1. 参数解析 ===== - order_id = request.data.get('orderId') - identity_type = request.data.get('identityType') # 前端必传:dashou / shangjia / boss - sender_name = request.data.get('senderName', '') - sender_avatar_raw = request.data.get('senderAvatar', '') - message_type = request.data.get('messageType', 'text') - message_text = request.data.get('text', '') - order_payload = request.data.get('orderPayload', None) - - logger.info(f"[群聊代理] 收到请求: orderId={order_id}, identityType={identity_type}, " - f"messageType={message_type}, text={message_text[:20]}...") - - if not order_id or not identity_type: - return Response({'code': 400, 'msg': '缺少 orderId 或 identityType'}) - if identity_type not in ('dashou', 'shangjia', 'boss'): - return Response({'code': 400, 'msg': 'identityType 无效'}) - - # ===== 2. 构造 senderId ===== - uid = request.user.yonghuid - prefix_map = {'dashou': 'Ds', 'shangjia': 'Sj', 'boss': 'Boss'} - sender_id = prefix_map[identity_type] + uid - logger.info(f"[群聊代理] 登录用户 yonghuid={uid}, 构造 senderId={sender_id}") - - # ===== 3. 查询订单 ===== - try: - order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=order_id) - except Dingdan.DoesNotExist: - logger.warning(f"[群聊代理] 订单 {order_id} 不存在") - return Response({'code': 404, 'msg': '订单不存在'}) - - # ===== 4. 权限校验 ===== - valid, err_msg = check_user_permission(identity_type, uid, order) - if not valid: - logger.warning(f"[群聊代理] 权限校验失败: {err_msg}, uid={uid}, identity={identity_type}") - return Response({'code': 403, 'msg': err_msg}) - - # ===== 5. 我方 GoEasy 配置 ===== - self_appkey = get_self_goeasy_appkey() - self_secret = get_self_goeasy_secret() - if not self_appkey: - logger.error("[群聊代理] 我方 GoEasy AppKey 未配置") - return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) - - # ===== 6. 公共群信息 ===== - real_group_id = f"group_{order.dingdan_id}" - group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else f"订单{order.dingdan_id}" - sender_avatar = full_avatar(sender_avatar_raw) if sender_avatar_raw else full_avatar('') - - # ===== 7. 订阅发送者到群(确保在群内) ===== - if not subscribe_users_to_group([sender_id], [real_group_id], self_appkey, self_secret): - logger.error("[群聊代理] 订阅群组失败") - return Response({'code': 500, 'msg': '订阅群组失败'}) - - # ===== 8. 判断跨平台状态 ===== - is_cross = order.is_cross - dispatch_type = order.dispatch_type # 1=我方派单, 2=对方派单 - logger.info(f"[群聊代理] 订单 {order_id} is_cross={is_cross}, dispatch_type={dispatch_type}") - - custom_payload = order_payload if message_type == 'order' else None - text_content = message_text - - # ---------- 非跨平台 或 我方派单 ---------- - if is_cross != 1 or dispatch_type == 1: - logger.info("[群聊代理] 走非跨平台/我方派单发送通道") - success = send_group_message( - appkey=self_appkey, - secret=self_secret, - group_id=real_group_id, - sender_id=sender_id, - sender_name=sender_name, - sender_avatar=sender_avatar, - message_text=text_content, - custom_payload=custom_payload, - group_name=group_name, - order_id=order.dingdan_id, - is_cross=1 if is_cross == 1 else 0 - ) - if success: - return Response({'code': 200, 'msg': '发送成功'}) - else: - return Response({'code': 500, 'msg': '消息发送失败'}) - - # ---------- 跨平台且对方派单 ---------- - logger.info("[群聊代理] 检测到跨平台对方派单,准备双平台发送") - partner_club_id = order.partner_club_id - partner_order_id = order.partner_order_id - - if not partner_club_id or not partner_order_id: - logger.warning("[群聊代理] 缺少 partner_club_id 或 partner_order_id,降级为本地发送") - success = send_group_message( - self_appkey, self_secret, real_group_id, sender_id, sender_name, - sender_avatar, text_content, custom_payload, group_name, order.dingdan_id, is_cross=1 - ) - return Response({'code': 200, 'msg': '发送成功(对方信息缺失)'} if success else ('发送失败', 500)) - - partner_cfg = get_partner_club_config(partner_club_id) - if not partner_cfg or not partner_cfg.chat_api_key: - logger.warning("[群聊代理] 对方俱乐部配置不可用,仅发送我方群") - success = send_group_message( - self_appkey, self_secret, real_group_id, sender_id, sender_name, - sender_avatar, text_content, custom_payload, group_name, order.dingdan_id, is_cross=1 - ) - return Response({'code': 200, 'msg': '发送成功(对方GoEasy未配置)'} if success else ('发送失败', 500)) - - partner_appkey = partner_cfg.chat_api_key - partner_secret = partner_cfg.chat_api_id or '' - partner_group_id = f"group_{partner_order_id}" - - # 构造代理身份(参考 chat_utils.py) - agent_sender_id = f"{order.dingdan_id}_{sender_id}" - if identity_type == 'dashou': - agent_sender_name = f"[我方]{sender_name}" - elif identity_type == 'shangjia': - agent_sender_name = f"[我方商家]{sender_name}" - else: - agent_sender_name = f"[我方老板]{sender_name}" - agent_avatar = full_avatar('') # 可定制 - - logger.info(f"[群聊代理] 对方群组={partner_group_id}, 代理身份={agent_sender_id}/{agent_sender_name}") - - # 1) 在我方群发送真实身份消息 - local_ok = send_group_message( - self_appkey, self_secret, real_group_id, sender_id, sender_name, - sender_avatar, text_content, custom_payload, group_name, order.dingdan_id, is_cross=1 - ) - logger.info(f"[群聊代理] 我方群发送结果: {local_ok}") - - # 2) 在对方群发送代理身份消息 - partner_ok = send_group_message( - partner_appkey, partner_secret, partner_group_id, - agent_sender_id, agent_sender_name, agent_avatar, - text_content, custom_payload, - '跨平台订单交流', partner_order_id, is_cross=1 - ) - logger.info(f"[群聊代理] 对方群发送结果: {partner_ok}") - - if local_ok and partner_ok: - return Response({'code': 200, 'msg': '发送成功(双平台)'}) - elif local_ok: - return Response({'code': 200, 'msg': '我方发送成功,对方发送失败'}) - else: - return Response({'code': 500, 'msg': '消息发送失败'})''' - - - -from peizhi.models import ClubConfig -from utils.chat_utils import establish_order_chat # 【新增】导入聊天工具 -class QiangdanView(APIView): - authentication_classes = [JWTAuthentication] - permission_classes = [IsAuthenticated] - - def post(self, request): - data = request.data - dingdan_id = data.get('dingdan_id') - if not dingdan_id: - return Response({'code': 400, 'msg': '订单ID不能为空'}, status=400) - - try: - with transaction.atomic(): - # 1. 验证打手身份和状态 - dashou_profile = self._validate_dashou_status(request.user) - if not dashou_profile: - return Response({'code': 403, 'msg': '您不是打手或账号状态异常'}, status=403) - - - # ==================== 【新增】罚单检查 ==================== - 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}) - # =========================================================== - - # 2. 锁定并验证订单(行锁) - order_result = self._validate_and_lock_order(dingdan_id, request.user.yonghuid) - if isinstance(order_result, str): - return Response({'code': 400, 'msg': order_result}) - order = order_result - - is_cross = order.is_cross == 1 - dispatch_type = order.dispatch_type - - if is_cross and dispatch_type == 2: - validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) - if not validation_result['success']: - return Response({'code': 400, 'msg': validation_result['message']}) - - claim_allowed = self._request_partner_check_claim(order, request.user.yonghuid) - if claim_allowed is None: - return Response({'code': 500, 'msg': '跨平台通信失败,请重试'}, status=500) - elif claim_allowed is False: - if order.zhuangtai == 1: - order.zhuangtai = 21 - order.save() - logger.info(f"订单 {dingdan_id} 被对方拒绝抢单,状态改为21") - else: - order.zhuangtai = 20 - order.save() - logger.info(f"订单 {dingdan_id} 已被其他平台抢走,状态改为20") - return Response({'code': 400, 'msg': '订单已被其他平台打手抢走或不可抢'}) - - self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) - - try: - update_daily_dispatch_stat( - direction=2, - partner_club_id=order.partner_club_id or '000001', - claimed_amount=order.jine, - claimed_count=1 - ) - except Exception as e: - logger.error(f"更新抢单统计失败: {e}", exc_info=True) - - elif is_cross and dispatch_type == 1: - validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) - if not validation_result['success']: - return Response({'code': 400, 'msg': validation_result['message']}) - - self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) - self._async_notify_partners_order_claimed(order) - - else: - validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) - if not validation_result['success']: - return Response({'code': 400, 'msg': validation_result['message']}) - self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) - - # ========== 【唯一改动】聊天建立方式 ========== - chat_success = establish_order_chat(dingdan_id) - - try: - update_dashou_daily_by_action( - yonghuid=order.jiedan_dashou_id, - amount=order.dashou_fencheng, - action=1 - ) - logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - - - # 在 QiangdanView.post 里,_execute_qiangdan 之后添加: - from dengji.services import record_dashou_biaoxian - # 记录抢单表现(不影响主流程) - # 如果订单表里有 bankuai 信息,可以用 order.bankuai 代替 None - record_dashou_biaoxian( - dingdan_id=dingdan_id, - dashou_id=order.jiedan_dashou_id, - xingwei=1, - jiedan_time=timezone.now(), # 直接取当前时间 - bankuai=None # 暂不填,后续提交时补上 - ) - - return Response({ - 'code': 200, - 'msg': '抢单成功', - 'data': { - 'dingdan_id': dingdan_id, - 'chat_established': chat_success - } - }) - - except Exception as e: - logger.error(f"抢单异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '系统异常,请稍后重试'}, status=500) - - # ---------- 以下所有方法原封不动照抄旧版 ---------- - def _validate_dashou_status(self, user): - try: - dashou_profile = user.dashou_profile - if (dashou_profile.zhuangtai != 1 or - dashou_profile.zhanghaozhuangtai != 1 or - dashou_profile.jifen < 5): - return None - return dashou_profile - except UserDashou.DoesNotExist: - return None - - def _validate_and_lock_order(self, dingdan_id, dashou_yonghuid): - try: - order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) - if order.zhuangtai not in [1, 7]: - return "订单状态不可抢" - if order.zhuangtai == 7: - if not order.zhiding_id or order.zhiding_id != dashou_yonghuid: - return "此订单为指定单,您不是指定打手" - if order.jiedan_dashou_id: - return "订单已被其他打手抢走" - return order - except Dingdan.DoesNotExist: - return "订单不存在" - - def _validate_order_requirements(self, order, dashou_profile, dashou_yonghuid): - if order.yaoqiuleixing == 1: - return self._validate_huiyuan_requirement(order, dashou_yonghuid) - elif order.yaoqiuleixing == 2: - return self._validate_yajin_requirement(order, dashou_profile) - return {'success': True, 'message': '验证通过'} - - def _validate_huiyuan_requirement(self, order, dashou_yonghuid): - try: - huiyuan_record = Huiyuangoumai.objects.get( - huiyuan_id=order.huiyuan_id, - yonghu_id=dashou_yonghuid - ) - if huiyuan_record.jiance_shifou_daoqi() or huiyuan_record.huiyuan_zhuangtai != 1: - return {'success': False, 'message': '会员已过期或未开通对应会员'} - return {'success': True, 'message': '会员验证通过'} - except Huiyuangoumai.DoesNotExist: - return {'success': False, 'message': '未开通对应会员,无法抢此订单'} - - def _validate_yajin_requirement(self, order, dashou_profile): - yajin_required = order.yongjin or 0 - dashou_yajin = dashou_profile.yajin or 0 - if dashou_yajin < yajin_required: - return {'success': False, 'message': f'押金不足,需要{yajin_required}元,当前有{dashou_yajin}元'} - return {'success': True, 'message': '押金验证通过'} - - def _execute_qiangdan(self, order, dashou_profile, dashou_yonghuid): - order.zhuangtai = 2 - order.jiedan_dashou_id = dashou_yonghuid - order.user2_id = f"Ds{dashou_yonghuid}" - order.save() - - dashou_profile.zhuangtai = 0 - dashou_profile.jiedanzongliang = F('jiedanzongliang') + 1 - dashou_profile.jinrijiedan = F('jinrijiedan') + 1 - dashou_profile.jinyuejiedan = F('jinyuejiedan') + 1 - dashou_profile.save() - - # ---------- 跨平台询问对方(照抄) ---------- - def _request_partner_check_claim(self, order, dashou_id): - partner_club_id = order.partner_club_id or '000001' - try: - club = Club.objects.filter(partner_club_id=partner_club_id).first() - if not club or not club.partner_domain: - logger.error(f"未找到对方俱乐部配置: partner_club_id={partner_club_id}") - return None - - my_club = Club.objects.filter(partner_club_id=club.partner_club_id).first() - if not my_club: - logger.error(f"未找到我方俱乐部配置: partner_club_id={club.partner_club_id}") - return None - - payload = { - 'token': settings.CROSS_PLATFORM_TOKEN, - 'wofangdingdan_id': order.partner_order_id, - 'partner_order_id': order.dingdan_id, - 'our_club_id': my_club.club_id, - 'partner_club_id': club.partner_club_id, - 'dashou_id': dashou_id, - 'sj_id': f"Sj{settings.AGENT_SHANGJIA_ID}" - } - - url = club.partner_domain.rstrip('/') + '/dingdan/partner_check_claim' - resp = requests.post(url, json=payload, timeout=5) - - if resp.status_code != 200: - logger.error(f"跨平台抢单询问HTTP失败: {resp.status_code}") - return None - - data = resp.json() - if data.get('code') == 0: - return data.get('data', {}).get('can_claim') is True - else: - logger.error(f"跨平台抢单询问业务失败: {data}") - return False - except Exception as e: - logger.error(f"跨平台抢单询问异常: {e}", exc_info=True) - return None - - # ---------- 异步通知其他俱乐部 ---------- - def _async_notify_partners_order_claimed(self, order): - def notify_task(): - clubs = Club.objects.filter(is_interop_enabled=True).select_related() - for club in clubs: - for attempt in range(1, 4): - success = self._send_claim_notification_to_partner(order, club) - if success: - break - if attempt < 3: - time.sleep(2 ** attempt) - else: - logger.error(f"通知俱乐部 {club.club_id} 订单已抢失败,重试3次后仍失败") - - thread = threading.Thread(target=notify_task) - thread.daemon = True - thread.start() - - def _send_claim_notification_to_partner(self, order, club): - try: - my_club = Club.objects.filter(partner_club_id=club.partner_club_id).first() - if not my_club: - logger.warning(f"未找到我方俱乐部配置: partner_club_id={club.partner_club_id}") - return False - - payload = { - 'token': settings.CROSS_PLATFORM_TOKEN, - 'our_club_id': my_club.club_id, - 'partner_club_id': club.partner_club_id, - 'partner_order_id': order.dingdan_id, - 'claimed': True - } - url = club.partner_domain.rstrip('/') + '/dingdan/partner_notify_claim' - resp = requests.post(url, json=payload, timeout=3) - return resp.status_code == 200 - except Exception as e: - logger.error(f"通知俱乐部 {club.club_id} 异常: {e}") - return False - - - - - - - -class DashouDingdanHuoquView1(APIView): - """ - 打手订单获取接口 - 支持筛选条件: - - zhuangtai_list: 订单状态列表(可选,空数组或不传表示全部状态) - - leixing_id: 商品类型ID(可选) - - keyword: 搜索关键词,模糊匹配订单ID和订单介绍(可选) - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取当前登录用户的 yonghuid - try: - current_user = request.user - yonghuid = current_user.yonghuid - except AttributeError: - return Response({ - 'code': 1, - 'msg': '用户认证失败', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 获取并校验前端参数 - try: - data = request.data - zhuangtai_list = data.get('zhuangtai_list', []) # 可选,默认空列表(全部) - page = max(1, int(data.get('page', 1))) - page_size = min(100, max(1, int(data.get('page_size', 10)))) - leixing_id = data.get('leixing_id', None) # 可选,商品类型ID - keyword = data.get('keyword', '').strip() # 可选,搜索关键词 - except (ValueError, TypeError): - return Response({ - 'code': 2, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 参数默认值与类型修正 - if not isinstance(zhuangtai_list, list): - zhuangtai_list = [] # 非列表则视为全部 - if leixing_id is not None: - try: - leixing_id = int(leixing_id) - except (ValueError, TypeError): - leixing_id = None - - # 4. 构建查询条件 - try: - # 基础条件:当前打手接单 - query = Q(jiedan_dashou_id=yonghuid) - - # 状态筛选:如果 zhuangtai_list 不为空列表,则加入筛选;否则查询全部状态 - if zhuangtai_list: - query &= Q(zhuangtai__in=zhuangtai_list) - - # 商品类型筛选 - if leixing_id is not None: - query &= Q(leixing_id=leixing_id) - - # 关键词搜索:模糊匹配订单ID和订单介绍 - if keyword: - keyword_query = Q(dingdan_id__icontains=keyword) | Q(jieshao__icontains=keyword) - query &= keyword_query - - # 5. 执行查询,按创建时间倒序 - dingdan_query = Dingdan.objects.filter(query).order_by('-create_time') - - # 只选择需要的字段,提高性能 - dingdan_query = dingdan_query.only( - 'dingdan_id', - 'zhuangtai', - 'dashou_fencheng', # 打手分成金额 - 'jieshao', - 'tupian', - 'nicheng', - 'create_time' - ) - - total_count = dingdan_query.count() - - # 6. 分页处理 - start_index = (page - 1) * page_size - end_index = start_index + page_size - current_page_orders = dingdan_query[start_index:end_index] - current_page_count = len(current_page_orders) - - # 7. 构建返回数据列表 - order_list = [] - for order in current_page_orders: - order_data = { - 'dingdan_id': order.dingdan_id, - 'zhuangtai': order.zhuangtai, - 'jine': float(order.dashou_fencheng) if order.dashou_fencheng else 0.0, - 'jieshao': order.jieshao or '', - 'tupian': order.tupian or '', - 'nicheng': order.nicheng or '', - 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '' - } - order_list.append(order_data) - - # 8. 判断是否还有更多数据 - has_more = (current_page_count == page_size) and (end_index < total_count) - if total_count == 0: - has_more = False - - return Response({ - 'code': 0, - 'msg': '成功', - 'data': { - 'list': order_list, - 'has_more': has_more, - 'total': total_count - } - }, 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({ - 'code': 4, - 'msg': '服务器内部错误', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - -'''class DashouDingdanHuoquView1(APIView): - """打手订单获取接口""" - permission_classes = [IsAuthenticated] # JWT认证 - - def post(self, request): - # 1. 获取当前登录用户的yonghuid - try: - current_user = request.user - yonghuid = current_user.yonghuid - - except AttributeError: - return Response({ - 'code': 1, - 'msg': '用户认证失败', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 获取和验证前端参数 - try: - data = request.data - zhuangtai_list = data.get('zhuangtai_list', []) - page = max(1, int(data.get('page', 1))) - page_size = min(100, max(1, int(data.get('page_size', 10)))) - - - - except (ValueError, TypeError): - return Response({ - 'code': 2, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 验证状态列表参数 - if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0: - return Response({ - 'code': 3, - 'msg': '状态列表不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - try: - # 4. 构建查询条件 - 直接查询订单主表 - # 条件1: 当前打手是接单打手 - # 条件2: 订单状态在指定列表中 - - # 注意:打手看不到状态1和7的订单(已下单、指定中) - # 但这里由前端控制传递的zhuangtai_list决定 - dashou_query = Q(jiedan_dashou_id=yonghuid) - zhuangtai_query = Q(zhuangtai__in=zhuangtai_list) - - # 5. 执行查询 - 只查询需要的字段,按创建时间倒序 - dingdan_query = Dingdan.objects.filter( - dashou_query & zhuangtai_query - ).order_by('-create_time') - - # 只选择需要的字段,提高性能 - dingdan_query = dingdan_query.only( - 'dingdan_id', - 'zhuangtai', - 'dashou_fencheng', # ✅ 关键:打手分成,不是jine - 'jieshao', - 'tupian', - 'nicheng', - 'create_time' - ) - - total_count = dingdan_query.count() - - - # 6. 计算分页 - start_index = (page - 1) * page_size - end_index = start_index + page_size - - # 获取当前页的订单数据 - current_page_orders = dingdan_query[start_index:end_index] - current_page_count = len(current_page_orders) - - # 7. 构建返回数据 - 只返回前端需要的字段 - order_list = [] - for order in current_page_orders: - order_data = { - # 核心字段 - 必须与前端完全对应 - 'dingdan_id': order.dingdan_id, # 订单ID - 'zhuangtai': order.zhuangtai, # 订单状态码 - # ✅ 关键:这里返回的是打手分成,不是订单金额 - 'jine': float(order.dashou_fencheng) if order.dashou_fencheng else 0.0, - 'jieshao': order.jieshao or '', # 订单介绍 - 'tupian': order.tupian or '', # 商品图片URL - 'nicheng': order.nicheng or '', # 游戏昵称 - 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '' - } - order_list.append(order_data) - - - - # 8. 判断是否还有更多数据 - # 条件1: 当前页获取了满page_size条数据 - # 条件2: 还有更多数据没取完 - has_more = current_page_count == page_size and end_index < total_count - - # 如果总数为0,直接没有更多 - if total_count == 0: - has_more = False - - - - return Response({ - 'code': 0, - 'msg': '成功', - 'data': { - 'list': order_list, # 订单列表 - 'has_more': has_more, # 是否还有更多数据 - 'total': total_count # 总订单数(可选) - } - }, 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({ - 'code': 4, - 'msg': '服务器内部错误', - 'data': None - }, 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上传凭证接口(使用腾讯云STS临时密钥)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - - - # 1. 接收参数 - dingdan_id = request.data.get('dingdan_id', '').strip() - if not dingdan_id: - return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) - - # 2. 业务验证逻辑(保持你的原有逻辑) - try: - from .models import Dingdan - dingdan = Dingdan.objects.get( - dingdan_id=dingdan_id, - jiedan_dashou_id=request.user.yonghuid, - zhuangtai=2 - ) - - except Dingdan.DoesNotExist: - return Response({'code': 5, 'msg': '订单无权限或状态不对', 'data': None}, - status=status.HTTP_403_FORBIDDEN) - except Exception as e: - - return Response({'code': 500, 'msg': '服务器验证异常', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 3. 获取腾讯云STS临时密钥 - try: - # 初始化固定凭证 - cred = credential.Credential( - settings.COS_SECRET_ID, - settings.COS_SECRET_KEY - ) - - # 创建STS客户端 - http_profile = HttpProfile() - http_profile.endpoint = "sts.tencentcloudapi.com" - client_profile = ClientProfile() - client_profile.httpProfile = http_profile - - client = sts_client.StsClient(cred, "ap-shanghai", client_profile) - - # 构建策略(允许上传到订单目录) - policy = { - "version": "2.0", - "statement": [ - { - "effect": "allow", - "action": [ - "cos:PutObject", - "cos:PostObject", - "cos:InitiateMultipartUpload", - "cos:UploadPart", - "cos:CompleteMultipartUpload" - ], - "resource": [ - f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*" - ] - } - ] - } - - # 请求STS临时凭证 - req = models.GetFederationTokenRequest() - req.Name = f"dashou_{dingdan_id}_{int(time.time())}" - req.Policy = json.dumps(policy) - req.DurationSeconds = 7200 # 2小时有效期 - - resp = client.GetFederationToken(req) - - # 提取临时密钥 - credentials = resp.Credentials - - - except TencentCloudSDKException as e: - logger.error(f"获取STS临时密钥失败: {str(e)}") - return Response({ - 'code': 500, - 'msg': f'获取上传凭证失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 4. 构建响应数据(严格遵循小程序COS SDK要求) - current_time = int(time.time()) - response_data = { - 'credentials': { - 'tmpSecretId': credentials.TmpSecretId, # 临时SecretId - 'tmpSecretKey': credentials.TmpSecretKey, # 临时SecretKey - 'sessionToken': credentials.Token, # 必须的Token - }, - 'startTime': current_time, - 'expiredTime': current_time + 7200, - 'bucket': settings.COS_BUCKET, - 'region': settings.COS_REGION, - 'upload_dir': f'order/{dingdan_id}/' - } - - - - # 5. 返回成功 - return Response({ - 'code': 0, - 'msg': '成功', - 'data': response_data - }, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f'COS凭证接口未知错误: {str(e)}', exc_info=True) - return Response({ - 'code': 500, - 'msg': f'服务器内部错误: {str(e)}', - 'data': None - }, 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上传凭证接口(调试版本)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 接收参数(修复 int 无 strip 问题) - dingdan_id = str(request.data.get('dingdan_id', '')).strip() - yongtu = request.data.get('yongtu', 'dingdan') - - # 2. 条件判断修正:除了 fakuan 之外,其他用途均需提供 dingdan_id - if yongtu != 'fakuan' and not dingdan_id: - return Response( - {'code': 400, 'msg': '订单ID不能为空', 'error': 'dingdan_id is required when yongtu is not fakuan'}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 3. 业务验证逻辑(根据用途分别验证) - if yongtu == 'chufa': - try: - from .models import Chufajilu - chufa_record = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - dashouid=request.user.yonghuid, - sqzhuangtai__in=[0, 3] - ).first() - if not chufa_record: - return Response( - {'code': 403, 'msg': '处罚记录无权限或状态不对', - 'error': f'No punishment record found for user {request.user.yonghuid} on order {dingdan_id}'}, - status=status.HTTP_403_FORBIDDEN - ) - except Exception as e: - logger.error(f"查处罚记录异常: {str(e)}") - return Response( - {'code': 500, 'msg': '服务器内部错误', 'error': str(e)}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - elif yongtu == 'fakuan': - # 罚款申诉不需要订单验证 - pass - else: - # 默认为订单相关,需要验证订单且打手必须接单 - try: - from .models import Dingdan - dingdan = Dingdan.objects.get( - dingdan_id=dingdan_id, - jiedan_dashou_id=request.user.yonghuid, - zhuangtai__in=[2, 8] - ) - except Dingdan.DoesNotExist: - return Response( - {'code': 403, 'msg': '此功能暂未开放', 'error': f'Order {dingdan_id} not found or not assigned to user {request.user.yonghuid}'}, - status=status.HTTP_403_FORBIDDEN - ) - - # 4. 获取腾讯云STS临时密钥 - try: - # 初始化固定凭证 - cred = credential.Credential( - settings.COS_SECRET_ID, - settings.COS_SECRET_KEY - ) - - # 创建STS客户端 - http_profile = HttpProfile() - http_profile.endpoint = "sts.tencentcloudapi.com" - client_profile = ClientProfile() - client_profile.httpProfile = http_profile - client = sts_client.StsClient(cred, "ap-shanghai", client_profile) - - # 构建策略资源 - resources = [] - if yongtu == 'chufa': - resources.append( - f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/dssstp/*" - ) - elif yongtu == 'fakuan': - resources.append( - f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/dashoufakuan/dashoufakuanshensu/*" - ) - else: - resources.append( - f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*" - ) - - policy = { - "version": "2.0", - "statement": [{ - "effect": "allow", - "action": [ - "cos:PutObject", - "cos:PostObject", - "cos:InitiateMultipartUpload", - "cos:UploadPart", - "cos:CompleteMultipartUpload" - ], - "resource": resources - }] - } - - req = models.GetFederationTokenRequest() - req.Name = f"dashou_{dingdan_id}_{int(time.time())}" - req.Policy = json.dumps(policy) - req.DurationSeconds = 7200 - - resp = client.GetFederationToken(req) - credentials = resp.Credentials - - except TencentCloudSDKException as e: - logger.error(f"获取STS临时密钥失败: {str(e)}") - return Response({ - 'code': 500, - 'msg': f'获取上传凭证失败: {str(e)}', - 'error_detail': str(e) - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 5. 构建响应 - current_time = int(time.time()) - upload_dir = 'fakuan/dashoufakuan/dashoufakuanshensu/' if yongtu == 'fakuan' else ( - 'a_long/chfajltp/dssstp/' if yongtu == 'chufa' else f'order/{dingdan_id}/' - ) - response_data = { - 'credentials': { - 'tmpSecretId': credentials.TmpSecretId, - 'tmpSecretKey': credentials.TmpSecretKey, - 'sessionToken': credentials.Token, - }, - 'startTime': current_time, - 'expiredTime': current_time + 7200, - 'bucket': settings.COS_BUCKET, - 'region': settings.COS_REGION, - 'upload_dir': upload_dir, - } - return Response({'code': 0, 'msg': '成功', 'data': response_data}) - - except Exception as e: - # 捕捉所有未预期的异常,返回完整 traceback - logger.error(f'COS凭证接口未知错误: {traceback.format_exc()}') - return Response({ - 'code': 500, - 'msg': f'服务器内部错误: {str(e)}', - 'error_detail': traceback.format_exc() - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - -'''class DashouCOSZhengshuView(APIView): - """打手获取COS上传凭证接口(使用腾讯云STS临时密钥)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 接收参数 - dingdan_id = str(request.data.get('dingdan_id', '')).strip() - yongtu = request.data.get('yongtu', 'dingdan') # 🔴【新增】用途参数:dingdan=订单,chufa=处罚 - - if not dingdan_id and yongtu is not 'fakuan': - return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) - - - - # 2. 业务验证逻辑 - try: - from .models import Dingdan - # 如果是处罚相关上传,验证处罚权限 - if yongtu == 'chufa': - # 验证处罚记录是否存在且属于当前打手 - from .models import Chufajilu - chufa_record = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - dashouid=request.user.yonghuid, - sqzhuangtai__in=[0, 3] # 待处罚或申诉中状态 - ).first() - - if not chufa_record: - return Response({'code': 5, 'msg': '处罚记录无权限或状态不对', 'data': None}, - status=status.HTTP_403_FORBIDDEN) - elif yongtu == 'fakuan': - pass - else: - # 原有订单验证逻辑 - dingdan = Dingdan.objects.get( - dingdan_id=dingdan_id, - jiedan_dashou_id=request.user.yonghuid, - zhuangtai__in=[2,8] - #zhuangtai=2 - ) - - except Dingdan.DoesNotExist: - return Response({'code': 5, 'msg': '此功能暂未开放', 'data': None}) - except Exception as e: - logger.error(f"订单验证异常: {str(e)}") - return Response({'code': 500, 'msg': '服务器验证异常', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 3. 获取腾讯云STS临时密钥 - try: - # 初始化固定凭证 - cred = credential.Credential( - settings.COS_SECRET_ID, - settings.COS_SECRET_KEY - ) - - # 创建STS客户端 - http_profile = HttpProfile() - http_profile.endpoint = "sts.tencentcloudapi.com" - client_profile = ClientProfile() - client_profile.httpProfile = http_profile - - client = sts_client.StsClient(cred, "ap-shanghai", client_profile) - - # 🔴【修复】构建策略(允许上传到多个路径) - resources = [] - - if yongtu == 'chufa': - # 处罚申诉图片路径 - resources.append( - f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/dssstp/*" - ) - - elif yongtu == 'fakuan': - # 🔥 罚款申诉图片专用路径 - resources.append(f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/dashoufakuan/dashoufakuanshensu/*") - else: - # 订单相关图片路径 - resources.append( - f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*" - ) - - policy = { - "version": "2.0", - "statement": [ - { - "effect": "allow", - "action": [ - "cos:PutObject", - "cos:PostObject", - "cos:InitiateMultipartUpload", - "cos:UploadPart", - "cos:CompleteMultipartUpload" - ], - "resource": resources - } - ] - } - - # 请求STS临时凭证 - req = models.GetFederationTokenRequest() - req.Name = f"dashou_{dingdan_id}_{int(time.time())}" - req.Policy = json.dumps(policy) - req.DurationSeconds = 7200 # 2小时有效期 - - resp = client.GetFederationToken(req) - - # 提取临时密钥 - credentials = resp.Credentials - - except TencentCloudSDKException as e: - logger.error(f"获取STS临时密钥失败: {str(e)}") - return Response({ - 'code': 500, - 'msg': f'获取上传凭证失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 4. 构建响应数据 - current_time = int(time.time()) - response_data = { - 'credentials': { - 'tmpSecretId': credentials.TmpSecretId, - 'tmpSecretKey': credentials.TmpSecretKey, - 'sessionToken': credentials.Token, - }, - 'startTime': current_time, - 'expiredTime': current_time + 7200, - 'bucket': settings.COS_BUCKET, - 'region': settings.COS_REGION, - 'upload_dir': 'fakuan/dashoufakuan/dashoufakuanshensu/' if yongtu == 'fakuan' else ( - 'a_long/chfajltp/dssstp/' if yongtu == 'chufa' else f'order/{dingdan_id}/') - } - - # 5. 返回成功 - return Response({ - 'code': 0, - 'msg': '成功', - 'data': response_data - }, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f'COS凭证接口未知错误: {str(e)}', exc_info=True) - return Response({ - 'code': 500, - 'msg': f'服务器内部错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' - - -class ShangjiaCOSZhengshuView(APIView): - """商家获取COS上传凭证接口(用于处罚申诉图片上传)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 接收参数 - dingdan_id = request.data.get('dingdan_id', '').strip() - yongtu = request.data.get('yongtu', 'chufa') # 用途:chufa=处罚申诉 - - if not dingdan_id: - return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 2. 🔴【修改】验证商家身份 - 通过反向查询shop_profile判断是否为商家 - try: - # 检查是否有商家扩展表 - shangjia_profile = request.user.shop_profile - - # 验证商家状态是否为1(正常) - if shangjia_profile.zhuangtai != 1: - return Response({'code': 3, 'msg': '商家账号状态异常,无法操作', 'data': None}, - status=status.HTTP_403_FORBIDDEN) - - # 🔴【重要】获取商家ID(从商家扩展表中获取) - shangjia_id = request.user.yonghuid # 假设商家扩展表中有yonghuid字段 - - except AttributeError: - # 没有shop_profile属性,说明不是商家 - return Response({'code': 3, 'msg': '非商家用户无权限', 'data': None}, - status=status.HTTP_403_FORBIDDEN) - - # 3. 🔴【修改】验证订单权限 - 通过商家订单扩展表验证 - try: - from .models import Dingdan, DingdanShangjia - - # 获取订单 - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) - - # 🔴【重要】通过反向查询商家扩展表验证订单是否属于当前商家 - try: - shangjia_kuozhan = dingdan.shangjia_kuozhan # 使用反向关系名 - - # 验证商家ID是否匹配 - if str(shangjia_kuozhan.shangjia_id) != str(shangjia_id): - return Response({'code': 2, 'msg': '订单不属于当前商家', 'data': None}, - status=status.HTTP_403_FORBIDDEN) - - # 验证订单状态是否允许申请处罚(根据前端逻辑) - allowed_zhuangtai = [2, 4, 5, 6, 8] - if dingdan.zhuangtai not in allowed_zhuangtai: - return Response({'code': 4, 'msg': '当前订单状态不允许申请处罚', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 🔴【新增】检查是否已存在处罚申请(通过Chufajilu模型) - # 注意:这里假设Chufajilu模型存在,并且有shangjia_id字段 - try: - from .models import Chufajilu - existing_chufa = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - qingqiuid=shangjia_id, - sqzhuangtai__in=[0, 1] # 审核中或已处罚状态 - ).exists() - - if existing_chufa: - return Response({'code': 5, 'msg': '该订单已有处罚申请,请勿重复上传', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - except ImportError: - # 如果没有Chufajilu模型,跳过这个检查 - pass - - except DingdanShangjia.DoesNotExist: - # 没有商家扩展记录,说明这不是商家订单 - return Response({'code': 2, 'msg': '非商家订单无法操作', 'data': None}, - status=status.HTTP_403_FORBIDDEN) - - except Dingdan.DoesNotExist: - return Response({'code': 2, 'msg': '订单不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND) - except Exception as e: - logger.error(f"订单验证异常: {str(e)}") - return Response({'code': 500, 'msg': '服务器验证异常', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 4. 获取腾讯云STS临时密钥 - try: - # 初始化固定凭证 - cred = credential.Credential( - settings.COS_SECRET_ID, - settings.COS_SECRET_KEY - ) - - # 创建STS客户端 - http_profile = HttpProfile() - http_profile.endpoint = "sts.tencentcloudapi.com" - client_profile = ClientProfile() - client_profile.httpProfile = http_profile - - client = sts_client.StsClient(cred, "ap-shanghai", client_profile) - - # 🔴【修改】构建策略 - 只允许上传到商家处罚证据图片目录 - resources = [ - f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/sjzjtp/*" - ] - - policy = { - "version": "2.0", - "statement": [ - { - "effect": "allow", - "action": [ - "cos:PutObject", - "cos:PostObject", - "cos:InitiateMultipartUpload", - "cos:UploadPart", - "cos:CompleteMultipartUpload" - ], - "resource": resources - } - ] - } - - # 请求STS临时凭证 - req = models.GetFederationTokenRequest() - # 🔴【修改】命名包含商家ID - req.Name = f"shangjia_{shangjia_id}_{dingdan_id}_{int(time.time())}" - req.Policy = json.dumps(policy) - req.DurationSeconds = 7200 # 2小时有效期 - - resp = client.GetFederationToken(req) - - # 提取临时密钥 - credentials = resp.Credentials - - except TencentCloudSDKException as e: - logger.error(f"商家获取STS临时密钥失败: {str(e)}") - return Response({ - 'code': 500, - 'msg': f'获取上传凭证失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 5. 构建响应数据 - current_time = int(time.time()) - response_data = { - 'credentials': { - 'tmpSecretId': credentials.TmpSecretId, - 'tmpSecretKey': credentials.TmpSecretKey, - 'sessionToken': credentials.Token, - }, - 'startTime': current_time, - 'expiredTime': current_time + 7200, - 'bucket': settings.COS_BUCKET, - 'region': settings.COS_REGION, - # 🔴【修改】商家专用上传目录 - 'upload_dir': 'a_long/chfajltp/sjzjtp/', - # 🔴【新增】返回商家ID和订单ID,便于前端生成文件名 - 'dingdan_id': dingdan_id, - 'shangjia_id': shangjia_id, - # 🔴【新增】可选:返回商家昵称,便于前端显示 - 'shangjia_nicheng': shangjia_profile.nicheng if hasattr(shangjia_profile, 'nicheng') else '' - } - - # 6. 返回成功 - return Response({ - 'code': 0, - 'msg': '成功', - 'data': response_data - }, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f'商家COS凭证接口未知错误: {str(e)}', exc_info=True) - return Response({ - 'code': 500, - 'msg': f'服务器内部错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -class DashouTijiaoView(APIView): - """打手提交订单完成接口""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取参数 - data = request.data - dingdan_id = data.get('dingdan_id', '') - liuyan = data.get('liuyan', '') - jiaofu_tupian_urls = data.get('jiaofu_tupian_urls', []) - - # 2. 参数验证 - if not dingdan_id: - return Response({ - 'code': 1, - 'msg': '订单ID不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if not isinstance(jiaofu_tupian_urls, list): - return Response({ - 'code': 2, - 'msg': '图片URL列表格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 限制图片数量(最多9张,最少1张) - if len(jiaofu_tupian_urls) == 0: - return Response({ - 'code': 3, - 'msg': '至少需要上传一张图片', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - if len(jiaofu_tupian_urls) > 9: - return Response({ - 'code': 4, - 'msg': '最多只能上传9张图片', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 留言长度限制(50字) - if liuyan and len(liuyan) > 50: - return Response({ - 'code': 5, - 'msg': '留言最多50个字', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 获取当前用户并验证打手身份 - current_user = request.user - yonghuid = current_user.yonghuid - - try: - dashou_profile = current_user.dashou_profile - except UserDashou.DoesNotExist: - return Response({ - 'code': 6, - 'msg': '用户不是打手身份', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 4. 验证订单状态和权限 - try: - dingdan = Dingdan.objects.get( - dingdan_id=dingdan_id, - jiedan_dashou_id=yonghuid, - zhuangtai=2 # 必须是进行中的订单 - ) - except Dingdan.DoesNotExist: - return Response({ - 'code': 7, - 'msg': '订单不存在或没有提交权限', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 5. 存储图片到Dashoutupian表 - for tupian_url in jiaofu_tupian_urls: - # 创建打手提交图片记录 - Dashoutupian.objects.create( - dingdan_id=dingdan_id, - dashou=yonghuid, - tupian=tupian_url, - create_time=timezone.now() - ) - - # 6. 更新订单主表 - dingdan.dashou_liuyan = liuyan - dingdan.zhuangtai = 8 # 状态改为结算中 - dingdan.update_time = timezone.now() - dingdan.save() - - # 7. 更新打手扩展表状态 - dashou_profile.zhuangtai = 1 # 改为数字1(根据你之前说的) - dashou_profile.save() - - from dengji.services import record_dashou_biaoxian - # ========== 新增:记录打手提交时间 ========== - record_dashou_biaoxian( - dingdan_id=dingdan_id, - dashou_id=yonghuid, - xingwei=2, # 提交 - tijiao_time=timezone.now() - ) - # =========================================== - - # 8. 返回成功响应 - return Response({ - 'code': 0, - 'msg': '订单提交成功', - 'data': { - 'dingdan_id': dingdan_id, - 'new_zhuangtai': 8, # 新的订单状态 - 'dashou_zhuangtai': 1 # 打手新的状态 - } - }, 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({ - 'code': 8, - 'msg': '服务器内部错误', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - - - - - -class DashouDingdanXiangqingView(APIView): - """打手订单详情获取接口""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取订单ID - dingdan_id = request.data.get('dingdan_id', '') - if not dingdan_id: - return Response({ - 'code': 1, - 'msg': '订单ID不能为空', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 获取当前用户并验证权限 - current_user = request.user - yonghuid = current_user.yonghuid - - try: - dashou_profile = current_user.dashou_profile - except UserDashou.DoesNotExist: - return Response({ - 'code': 2, - 'msg': '用户不是打手身份', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 3. 查询订单主表 - try: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response({ - 'code': 3, - 'msg': '订单不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - # 4. 验证订单是否属于当前打手 - if dingdan.jiedan_dashou_id != yonghuid: - return Response({ - 'code': 4, - 'msg': '没有权限查看此订单', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - # 5. 构建基础订单数据 - dingdan_data = { - 'dingdan_id': dingdan.dingdan_id, - 'zhuangtai': dingdan.zhuangtai, - 'jine': float(dingdan.dashou_fencheng) if dingdan.dashou_fencheng else 0.0, - 'jieshao': dingdan.jieshao or '', - 'tupian': dingdan.tupian or '', - 'nicheng': dingdan.nicheng or '', - 'beizhu': dingdan.beizhu or '', - 'create_time': dingdan.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan.create_time else '', - 'tuikuan_liyou':dingdan.tkly or '', - 'dashou_liuyan': dingdan.dashou_liuyan or '', - 'fuwudashou_id': dingdan.jiedan_dashou_id or '', - 'fadanpingtai': dingdan.fadan_pingtai or '' - } - - # 6. 查询打手提交的图片 - dashou_tupian_list = Dashoutupian.objects.filter( - dingdan_id=dingdan_id, - dashou=yonghuid - ).order_by('create_time') - - jiaofu_tupian_urls = [] - for tupian_record in dashou_tupian_list: - if tupian_record.tupian: - jiaofu_tupian_urls.append(tupian_record.tupian) - - dingdan_data['dashou_jiaofu'] = jiaofu_tupian_urls - - # 7. 查询商家扩展表信息 - shangjia_info = {} - try: - shangjia_kuozhan = dingdan.shangjia_kuozhan - shangjia_info = { - 'shangjia_id': shangjia_kuozhan.shangjia_id or '', - 'shangjia_nicheng': shangjia_kuozhan.sjnicheng or '', - 'shangjia_liuyan': shangjia_kuozhan.shangjia_pingjia or '', - #'tuikuan_liyou': '', - 'chufa_liyou': shangjia_kuozhan.cfliyou or '', - 'chufa_zhuangtai': shangjia_kuozhan.sqzhuangtai or 0, - #'chufa_jieguo': shangjia_kuozhan.chufa_jieguo or '', - 'bohui_liyou': shangjia_kuozhan.bhliyou or '' - } - except DingdanShangjia.DoesNotExist: - # 没有商家扩展信息,返回空值 - shangjia_info = { - 'shangjia_id': '', - 'shangjia_nicheng': '', - 'shangjia_liuyan': '', - 'tuikuan_liyou': '', - 'chufa_liyou': '', - 'chufa_zhuangtai': 0, - 'chufa_jieguo': '', - 'bohui_liyou': '' - } - - # 8. 查询平台扩展表信息(如果有的话) - pingtai_info = {} - try: - pingtai_kuozhan = dingdan.pingtai_kuozhan - pingtai_info = { - 'laoban_id': pingtai_kuozhan.laoban_id or '', - 'laoban_pingjia': pingtai_kuozhan.laoban_pingjia or '' - } - except DingdanPingtai.DoesNotExist: - pingtai_info = { - 'laoban_id': '', - 'laoban_pingjia': '' - } - - # 9. 合并所有数据 - response_data = { - **dingdan_data, - **shangjia_info, - **pingtai_info - } - - return Response({ - 'code': 0, - 'msg': '成功', - 'data': response_data - }, 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({ - 'code': 5, - 'msg': '服务器内部错误', - 'data': None - }, 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): - """ - 后台订单详情获取接口 - 权限: JWT Token认证 + 管理员身份验证 - 前端调用: POST /dingdan/adddxq - 参数: {"zhanghao": "管理员手机号", "dingdan_id": "订单ID"} - """ - - 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() - - if not qianzhanghao or not dingdan_id: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2. JWT Token身份验证 (IsAuthenticated已处理) - 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. 查询订单主表 - try: - from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia, Dashoutupian - - # 查询订单主表 - 使用select_related预加载关联表 - dingdan_obj = Dingdan.objects.select_related( - 'pingtai_kuozhan', 'shangjia_kuozhan' - ).get(dingdan_id=dingdan_id) - - except Dingdan.DoesNotExist: - return Response( - {'code': 404, 'message': '订单不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 7. 构建返回数据 - 主表字段 - response_data = { - # 基础信息字段 - 'dingdan_id': dingdan_obj.dingdan_id or '', - 'jieshao': dingdan_obj.jieshao or '', - 'zhuangtai': dingdan_obj.zhuangtai or 0, - 'leixing': dingdan_obj.leixing_id or 0, # 前端需要 leixing,对应 leixing_id - 'fadanpingtai': dingdan_obj.fadan_pingtai or 0, # 前端需要 fadanpingtai,对应 fadan_pingtai - 'jiage': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00, # 前端需要 jiage,对应 jine - 'tupian_url': dingdan_obj.tupian or '', # 前端需要 tupian_url,对应 tupian - 'beizhu': dingdan_obj.beizhu or '', - - # 服务信息字段 - 'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '', - 'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00, - 'zhiding_id': dingdan_obj.zhiding_id or '', - 'dashou_liuyan': dingdan_obj.dashou_liuyan 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 '', - } - - # 8. 根据发单平台查询扩展表数据 - fadan_pingtai = dingdan_obj.fadan_pingtai - - if fadan_pingtai == 1: - # 平台发单 - 查询平台订单扩展表 - try: - # 使用反向关联 pingtai_kuozhan - pingtai_kuozhan = dingdan_obj.pingtai_kuozhan - - response_data.update({ - 'laoban_id': pingtai_kuozhan.laoban_id or '', - 'nicheng': dingdan_obj.nicheng or '', # 这个字段在订单主表 - 'laoban_pingjia': pingtai_kuozhan.laoban_pingjia or '', - }) - except DingdanPingtai.DoesNotExist: - # 扩展表不存在,返回空值 - response_data.update({ - 'laoban_id': '', - 'nicheng': dingdan_obj.nicheng or '', - 'laoban_pingjia': '', - }) - - elif fadan_pingtai == 2: - # 商家发单 - 查询商家订单扩展表 - try: - # 使用反向关联 shangjia_kuozhan - shangjia_kuozhan = dingdan_obj.shangjia_kuozhan - - response_data.update({ - 'shangjia_id': shangjia_kuozhan.shangjia_id or '', - 'sjnicheng': shangjia_kuozhan.sjnicheng or '', - 'shangjia_pingjia': shangjia_kuozhan.shangjia_pingjia or '', - 'cfliyou': shangjia_kuozhan.cfliyou or '', # 商家处罚理由 - 'bhliyou': shangjia_kuozhan.bhliyou or '', # 驳回理由 - }) - except DingdanShangjia.DoesNotExist: - # 扩展表不存在,返回空值 - response_data.update({ - 'shangjia_id': '', - 'sjnicheng': '', - 'shangjia_pingjia': '', - 'cfliyou': '', - 'bhliyou': '', - }) - - # 9. 查询打手提交的图片 - try: - # 查询该订单的所有打手提交图片 - dashou_tupian_list = Dashoutupian.objects.filter( - dingdan_id=dingdan_id - ).values_list('tupian', flat=True) - - # 转换为列表,过滤空值 - dashou_images = [img for img in dashou_tupian_list if img] - response_data['dashou_images'] = dashou_images - except Exception as e: - logger.warning(f"查询打手图片失败: {str(e)}") - response_data['dashou_images'] = [] - - # 🔥 新增:查询退款记录表中的商家退款理由 - try: - # 导入退款记录模型 - from dingdan.models import Tuikuanjilu - - # 查询该订单的退款记录(一个订单可能有多次退款申请,我们取最新的) - tuikuan_record = Tuikuanjilu.objects.filter( - dingdan_id=dingdan_id - ).order_by('-create_time').first() # 按创建时间倒序,取最新的 - - if tuikuan_record and tuikuan_record.liyou: - # 如果有退款记录且理由不为空,添加到返回数据 - response_data['tuikuan_liyou'] = tuikuan_record.liyou - else: - # 没有找到退款记录或理由为空 - response_data['tuikuan_liyou'] = '' - except Tuikuanjilu.DoesNotExist: - # 退款记录表不存在该订单的记录 - response_data['tuikuan_liyou'] = '' - except Exception as e: - # 查询过程中出现其他异常 - logger.warning(f"查询退款记录失败: {str(e)}") - response_data['tuikuan_liyou'] = '' - - # 10. 返回成功响应 - 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 - ) - - - - -# 获取日志器 -logger = logging.getLogger(__name__) - - -class AdGengHuanDaShou(APIView): - """ - 后台更换打手接口 - 权限: JWT Token认证 + 管理员身份验证 - 前端调用: POST /dingdan/adddgh - 参数: { - "zhanghao": "管理员账号", - "dingdan_id": "订单ID", - "new_dashou_id": "新打手ID" - } - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - """ - 更换订单的打手 - 核心逻辑: - 1. 验证管理员身份 - 2. 验证订单存在且状态为进行中(2)或结算中(8) - 3. 验证新打手ID存在且是打手身份 - 4. 验证新打手状态正常(zhuangtai=1且zhanghaozhuangtai=1) - 5. 根据订单的抢单要求类型进行验证: - - 会员抢单(1):验证打手有对应会员且未过期 - - 押金抢单(2):验证打手押金≥订单押金 - 6. 更新新打手状态和统计数据 - 7. 更新原打手状态和统计数据 - 8. 更新订单信息 - """ - try: - # 1. 获取前端参数 - qianzhanghao = request.data.get('zhanghao', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - new_dashou_id = request.data.get('new_dashou_id', '').strip() - - if not qianzhanghao or not dingdan_id or not new_dashou_id: - 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. 查询订单主表 - try: - from dingdan.models import Dingdan - dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response( - {'code': 404, 'message': '订单不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 7. 验证订单状态是否为进行中(2)或结算中(8) - if dingdan_obj.zhuangtai not in [2, 8]: - return Response( - {'code': 400, 'message': '订单状态不允许更换打手', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 8. 验证新打手ID是否存在,并且是打手身份 - try: - new_dashou_user = UserMain.objects.get( - yonghuid=new_dashou_id, - #user_type='dashou' - ) - except UserMain.DoesNotExist: - return Response( - {'code': 404, 'message': '打手不存在或不是打手身份', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 9. 获取新打手扩展表 - try: - new_dashou_profile = new_dashou_user.dashou_profile - except AttributeError: - return Response( - {'code': 404, 'message': '打手信息不完整', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 10. 验证新打手状态:zhuangtai=1且zhanghaozhuangtai=1 - if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1: - return Response( - {'code': 400, 'message': '打手状态不允许接单', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 11. 根据订单的抢单要求类型进行验证 - yaoqiuleixing = dingdan_obj.yaoqiuleixing - - if yaoqiuleixing == 1: # 会员抢单 - # 获取订单的会员ID - huiyuan_id = dingdan_obj.huiyuan_id - if not huiyuan_id: - return Response( - {'code': 400, 'message': '订单会员ID为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 查询会员购买记录 - try: - from shangpin.models import Huiyuangoumai - huiyuan_goumai = Huiyuangoumai.objects.get( - huiyuan_id=huiyuan_id, - yonghu_id=new_dashou_id # 注意:yonghu_id存储的是用户ID(yonghuid) - ) - except Huiyuangoumai.DoesNotExist: - return Response( - {'code': 400, 'message': '打手未购买该订单要求的会员', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 检查会员是否过期 - # ✅ 可以调用!因为jiance_shifou_daoqi()是Huiyuangoumai模型的实例方法 - is_expired = huiyuan_goumai.jiance_shifou_daoqi() - if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1: - return Response( - {'code': 400, 'message': '打手的会员已过期', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - elif yaoqiuleixing == 2: # 押金抢单 - # 获取订单的押金(yongjin字段) - order_yajin = dingdan_obj.yongjin - if order_yajin is None: - return Response( - {'code': 400, 'message': '订单押金为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证打手押金是否大于等于订单押金 - if new_dashou_profile.yajin < order_yajin: - return Response( - {'code': 400, 'message': '打手押金不足', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - else: - # 未知的抢单要求类型 - return Response( - {'code': 400, 'message': '订单抢单要求类型错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 12. 所有验证通过,开始更换打手(使用事务保证一致性) - with transaction.atomic(): - # 12.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() - - # 12.2 如果原订单有接单打手,则更新原打手状态 - old_dashou_id = dingdan_obj.jiedan_dashou_id - if old_dashou_id: - try: - # 查询原打手主表 - old_dashou_user = UserMain.objects.get( - yonghuid=old_dashou_id, - #user_type='dashou' - ) - - # 获取原打手扩展表 - 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: - # 原打手不存在,继续执行不影响更换 - logger.warning(f"原打手{old_dashou_id}不存在,跳过更新") - except AttributeError: - # 原打手扩展表不存在,继续执行不影响更换 - logger.warning(f"原打手{old_dashou_id}扩展信息不存在,跳过更新") - except Exception as e: - # 其他异常,记录日志但继续执行 - logger.error(f"更新原打手状态异常: {str(e)}", exc_info=True) - - # 12.3 更新订单表 - dingdan_obj.jiedan_dashou_id = new_dashou_id - dingdan_obj.zhuangtai = 2 # 进行中状态 - dingdan_obj.save() - - # 13. 返回成功响应 - return Response({ - 'code': 0, - 'message': '更换打手成功', - 'data': None - }) - - 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 logging - -# 获取日志器 -logger = logging.getLogger(__name__) - - -class AdQiangZhiJieDan(APIView): - """ - 后台强制结单接口 - 权限: JWT Token认证 + 管理员身份验证 - 前端调用: POST /dingdan/adddqzjd - 参数: { - "zhanghao": "管理员账号", - "dingdan_id": "订单ID" - } - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - """ - 强制结单接口 - 核心逻辑: - 1. 验证管理员身份 - 2. 验证订单状态为2(进行中)、8(结算中)或4(退款审核中)才能强制结单 - 3. 根据发单平台(1=平台发单, 2=商家发单)分别处理 - 4. 更新订单状态为3(已完成) - 5. 给打手结算分成金额 - 6. 如果是商家发单,还需要更新商家成交订单数量 - """ - try: - # 1. 获取前端参数 - qianzhanghao = request.data.get('zhanghao', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - - if not qianzhanghao or not dingdan_id: - 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. 查询订单主表 - try: - from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia - dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response( - {'code': 404, 'message': '订单不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 7. 验证订单状态是否为2、8或4 - if dingdan_obj.zhuangtai not in [2, 8, 4]: - return Response( - {'code': 400, 'message': '订单状态不允许强制结单', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 8. 获取订单的重要字段 - jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID - dashou_fencheng = dingdan_obj.dashou_fencheng # 打手分成金额 - fadan_pingtai = dingdan_obj.fadan_pingtai # 发单平台 - - # 9. 验证打手分成金额是否存在 - if dashou_fencheng is None: - return Response( - {'code': 400, 'message': '订单缺少打手分成金额', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 10. 使用事务保证数据一致性 - with transaction.atomic(): - # 10.1 更新订单状态为3(已完成) - dingdan_obj.zhuangtai = 3 - dingdan_obj.save() - - # 10.2 处理打手结算 - if jiedan_dashou_id: - try: - # 查询打手用户主表 - dashou_user = UserMain.objects.get( - yonghuid=jiedan_dashou_id, - #user_type='dashou' - ) - - # 获取打手扩展表 - dashou_profile = dashou_user.dashou_profile - - # 更新打手扩展表数据 - # ✅ 确认字段:chengjiaozongliang 成交单总量 - dashou_profile.chengjiaozongliang += 1 - - # ✅ 确认字段:yue 可提现余额 - dashou_profile.yue += dashou_fencheng - - # ✅ 确认字段:zonge 打单赚取总额 - dashou_profile.zonge += dashou_fencheng - - # ✅ 确认字段:jinrishouyi 今日收益金额 - dashou_profile.jinrishouyi += dashou_fencheng - - # ✅ 确认字段:jinyueshouyi 今月收益金额 - dashou_profile.jinyueshouyi += dashou_fencheng - - dashou_profile.save() - logger.info(f"强制结单成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}") - - except UserMain.DoesNotExist: - # 打手用户不存在,记录日志但不影响强制结单 - logger.warning(f"强制结单:打手{jiedan_dashou_id}不存在,跳过打手结算") - except AttributeError: - # 打手扩展表不存在,记录日志但不影响强制结单 - logger.warning(f"强制结单:打手{jiedan_dashou_id}扩展信息不存在,跳过打手结算") - - # 10.3 如果是商家发单,还需要更新商家扩展表 - if fadan_pingtai == 2: - try: - # 获取商家订单扩展表 - shangjia_kuozhan = dingdan_obj.shangjia_kuozhan - shangjia_id = shangjia_kuozhan.shangjia_id - - if shangjia_id: - # 查询商家用户主表 - shangjia_user = UserMain.objects.get( - yonghuid=shangjia_id, - #user_type='shop' - ) - - # 获取商家扩展表 - shangjia_profile = shangjia_user.shop_profile - - # ✅ 确认字段:chengjiao 商家成交订单总量 - shangjia_profile.chengjiao += 1 - shangjia_profile.save() - - logger.info(f"强制结单成功,商家{shangjia_id}成交订单+1") - - except DingdanShangjia.DoesNotExist: - # 商家订单扩展表不存在,跳过商家更新 - logger.warning(f"强制结单:订单{dingdan_id}商家扩展表不存在,跳过商家更新") - except UserMain.DoesNotExist: - # 商家用户不存在,跳过商家更新 - logger.warning(f"强制结单:商家{shangjia_id}不存在,跳过商家更新") - except AttributeError: - # 商家扩展表不存在,跳过商家更新 - logger.warning(f"强制结单:商家{shangjia_id}扩展信息不存在,跳过商家更新") - - # 12. 更新平台收益流水(仅在平台发单时) - if dingdan_obj.fadan_pingtai == 1: - try: - from dingdan.utils import update_platform_profit # 导入函数 - update_platform_profit(dingdan_id) - except Exception as e: - # 平台收益更新失败不影响主流程,但记录日志 - logger.error(f"强制结单后更新平台收益失败: {str(e)}") - - # 11. 返回成功响应 - return Response({ - 'code': 0, - 'message': '强制结单成功', - 'data': None - }) - - 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 AdJuJueTuiKuan(APIView): - """ - 后台拒绝退款接口 - 权限: JWT Token认证 + 管理员身份验证 - 前端调用: POST /dingdan/adddjj - 参数: { - "zhanghao": "管理员账号", - "dingdan_id": "订单ID", - "jujue_liyou": "拒绝退款理由(可选)" - } - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - """ - 拒绝退款接口 - 核心逻辑: - 1. 验证管理员身份 - 2. 拒绝退款后订单状态改为6(退款失败) - 3. 给打手结算分成金额 - 4. 如果是商家发单,更新商家成交订单数量 - 5. 更新退款记录表,状态改为2(驳回) - """ - try: - # 1. 获取前端参数 - qianzhanghao = request.data.get('zhanghao', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - jujue_liyou = request.data.get('jujue_liyou', '').strip() - - if not qianzhanghao or not dingdan_id: - 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. 查询订单主表 - try: - from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu - dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response( - {'code': 404, 'message': '订单不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 7. 获取订单的重要字段 - jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID - dashou_fencheng = dingdan_obj.dashou_fencheng # 打手分成金额 - fadan_pingtai = dingdan_obj.fadan_pingtai # 发单平台 - - # 8. 验证打手分成金额是否存在 - if dashou_fencheng is None: - return Response( - {'code': 400, 'message': '订单缺少打手分成金额', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 9. 使用事务保证数据一致性 - with transaction.atomic(): - # 9.1 更新订单状态为6(退款失败) - dingdan_obj.zhuangtai = 6 - dingdan_obj.save() - - # 9.2 处理打手结算 - if jiedan_dashou_id: - try: - # 查询打手用户主表 - dashou_user = UserMain.objects.get( - yonghuid=jiedan_dashou_id, - #user_type='dashou' - ) - - # 获取打手扩展表 - dashou_profile = dashou_user.dashou_profile - - # 更新打手扩展表数据 - # ✅ 确认字段:chengjiaozongliang 成交单总量 - dashou_profile.chengjiaozongliang += 1 - - # ✅ 确认字段:yue 可提现余额 - dashou_profile.yue += dashou_fencheng - - # ✅ 确认字段:zonge 打单赚取总额 - dashou_profile.zonge += dashou_fencheng - - # ✅ 确认字段:jinrishouyi 今日收益金额 - dashou_profile.jinrishouyi += dashou_fencheng - - # ✅ 确认字段:jinyueshouyi 今月收益金额 - dashou_profile.jinyueshouyi += dashou_fencheng - - dashou_profile.save() - logger.info(f"拒绝退款成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}") - - except UserMain.DoesNotExist: - # 打手用户不存在,记录日志但不影响操作 - logger.warning(f"拒绝退款:打手{jiedan_dashou_id}不存在,跳过打手结算") - except AttributeError: - # 打手扩展表不存在,记录日志但不影响操作 - logger.warning(f"拒绝退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手结算") - - # 9.3 如果是商家发单,还需要更新商家扩展表 - if fadan_pingtai == 2: - try: - # 获取商家订单扩展表 - shangjia_kuozhan = dingdan_obj.shangjia_kuozhan - shangjia_id = shangjia_kuozhan.shangjia_id - - if shangjia_id: - # 查询商家用户主表 - shangjia_user = UserMain.objects.get( - yonghuid=shangjia_id, - #user_type='shop' - ) - - # 获取商家扩展表 - shangjia_profile = shangjia_user.shop_profile - - # ✅ 确认字段:chengjiao 商家成交订单总量 - shangjia_profile.chengjiao += 1 - shangjia_profile.save() - - logger.info(f"拒绝退款成功,商家{shangjia_id}成交订单+1") - - except DingdanShangjia.DoesNotExist: - # 商家订单扩展表不存在,跳过商家更新 - logger.warning(f"拒绝退款:订单{dingdan_id}商家扩展表不存在,跳过商家更新") - except UserMain.DoesNotExist: - # 商家用户不存在,跳过商家更新 - logger.warning(f"拒绝退款:商家{shangjia_id}不存在,跳过商家更新") - except AttributeError: - # 商家扩展表不存在,跳过商家更新 - logger.warning(f"拒绝退款:商家{shangjia_id}扩展信息不存在,跳过商家更新") - - # 9.4 更新退款记录表 - try: - # 查询退款记录表 - tuikuan_jilu = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) - - # ✅ 确认字段:sqzhuangtai 申请状态,改为2(驳回) - tuikuan_jilu.sqzhuangtai = 2 - - # ✅ 确认字段:bhliyou 驳回理由 - tuikuan_jilu.bhliyou = jujue_liyou - - # ✅ 确认字段:chuliid 处理人ID(使用管理员账号,即手机号) - tuikuan_jilu.chuliid = qianzhanghao - - tuikuan_jilu.save() - logger.info(f"拒绝退款成功,更新退款记录表,订单:{dingdan_id}") - - except Tuikuanjilu.DoesNotExist: - # 退款记录表不存在,记录日志但不影响主流程 - logger.warning(f"拒绝退款:订单{dingdan_id}退款记录不存在,跳过更新") - - # 10. 返回成功响应 - return Response({ - 'code': 0, - 'message': '拒绝退款成功', - 'data': None - }) - - 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 AdTongYiTuiKuanShangJia(APIView): - """ - 后台同意退款接口(商家发单) - 权限: JWT Token认证 + 管理员身份验证 + 密码验证 - 前端调用: POST /dingdan/adddsjtk - 参数: { - "zhanghao": "管理员账号", - "dingdan_id": "订单ID", - "tuikuan_liyou": "退款理由(可选)", - "pay_password": "支付密码(openid)" - } - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - """ - 同意退款接口(商家发单) - 核心逻辑: - 1. 验证管理员身份 - 2. 验证支付密码(openid) - 3. 检查订单状态为4(退款审核中) - 4. 更新订单状态为5(已退款) - 5. 更新打手退款订单数量 - 6. 更新商家退款订单数量和余额(加上订单金额) - 7. 更新退款记录表,状态改为1(成功) - """ - try: - # 1. 获取前端参数 - qianzhanghao = request.data.get('zhanghao', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() - pay_password = request.data.get('pay_password', '').strip() - - if not qianzhanghao or not dingdan_id or not pay_password: - 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. 验证支付密码(对比openid) - if pay_password != dangqianyonghu.openid: - return Response( - {'code': 401, 'message': '支付密码错误', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 7. 查询订单主表 - try: - from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu - dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response( - {'code': 404, 'message': '订单不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 8. 验证订单状态为4(退款审核中) - if dingdan_obj.zhuangtai != 4: - return Response( - {'code': 400, 'message': '订单状态不是退款审核中', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 9. 验证订单为商家发单 - if dingdan_obj.fadan_pingtai != 2: - return Response( - {'code': 400, 'message': '此接口只处理商家发单的退款', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 10. 获取订单的重要字段 - jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID - jine = dingdan_obj.jine # 订单金额(不是打手分成) - - # 11. 验证订单金额是否存在 - if jine is None: - return Response( - {'code': 400, 'message': '订单缺少订单金额', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 12. 获取商家ID - try: - shangjia_kuozhan = dingdan_obj.shangjia_kuozhan - shangjia_id = shangjia_kuozhan.shangjia_id - except DingdanShangjia.DoesNotExist: - return Response( - {'code': 400, 'message': '订单商家信息不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 13. 使用事务保证数据一致性 - with transaction.atomic(): - # 13.1 更新订单状态为5(已退款) - dingdan_obj.zhuangtai = 5 - dingdan_obj.save() - - # 13.2 更新打手扩展表(退款订单总量+1) - if jiedan_dashou_id: - try: - # 查询打手用户主表 - dashou_user = UserMain.objects.get( - yonghuid=jiedan_dashou_id, - #user_type='dashou' - ) - - # 获取打手扩展表 - dashou_profile = dashou_user.dashou_profile - - # ✅ 确认字段:tuikuanliang 退款订单总量 - dashou_profile.tuikuanliang += 1 - - dashou_profile.save() - logger.info(f"同意退款成功,打手{jiedan_dashou_id}退款订单+1") - - except UserMain.DoesNotExist: - # 打手用户不存在,记录日志但不影响操作 - logger.warning(f"同意退款:打手{jiedan_dashou_id}不存在,跳过打手更新") - except AttributeError: - # 打手扩展表不存在,记录日志但不影响操作 - logger.warning(f"同意退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手更新") - - # 13.3 更新商家扩展表 - if shangjia_id: - try: - # 查询商家用户主表 - shangjia_user = UserMain.objects.get( - yonghuid=shangjia_id, - #user_type='shop' - ) - - # 获取商家扩展表 - shangjia_profile = shangjia_user.shop_profile - - # ✅ 确认字段:tuikuan 退款订单总量 - shangjia_profile.tuikuan += 1 - - # ✅ 确认字段:yue 商家余额(加上订单金额) - shangjia_profile.yue += jine - - shangjia_profile.save() - logger.info(f"同意退款成功,商家{shangjia_id}退款订单+1,余额增加:{jine}") - - except UserMain.DoesNotExist: - # 商家用户不存在,记录日志但不影响操作 - logger.warning(f"同意退款:商家{shangjia_id}不存在,跳过商家更新") - except AttributeError: - # 商家扩展表不存在,记录日志但不影响操作 - logger.warning(f"同意退款:商家{shangjia_id}扩展信息不存在,跳过商家更新") - - # 13.4 更新退款记录表 - try: - # 查询退款记录表 - tuikuan_jilu = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) - - # ✅ 确认字段:sqzhuangtai 申请状态,改为1(成功) - tuikuan_jilu.sqzhuangtai = 1 - - # 可以添加同意理由字段,如:tongyi_liyou - # tuikuan_jilu.tongyi_liyou = tuikuan_liyou - - # ✅ 确认字段:chuliid 处理人ID(使用管理员账号,即手机号) - tuikuan_jilu.chuliid = qianzhanghao - - tuikuan_jilu.save() - logger.info(f"同意退款成功,更新退款记录表,订单:{dingdan_id}") - - except Tuikuanjilu.DoesNotExist: - # 退款记录表不存在,记录日志但不影响主流程 - logger.warning(f"同意退款:订单{dingdan_id}退款记录不存在,跳过更新") - - # 14. 返回成功响应 - return Response({ - 'code': 0, - 'message': '同意退款成功', - 'data': None - }) - - 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 - ) - - - - - - - - - -# 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): - """ - 后台同意退款接口(平台发单) - 权限: JWT Token认证 + 管理员身份验证 + 密码验证 - 前端调用: POST /dingdan/adddpttk - 参数: { - "zhanghao": "管理员账号", - "dingdan_id": "订单ID", - "tuikuan_liyou": "退款理由(可选)", - "pay_password": "支付密码(openid)" - } - """ - 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() - tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() - pay_password = request.data.get('pay_password', '').strip() - - if not qianzhanghao or not dingdan_id or not pay_password: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2. JWT Token身份验证 - dangqianyonghu = request.user - - # 3. 验证手机号是否匹配 - if str(getattr(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. 验证支付密码(对比openid) - if pay_password != dangqianyonghu.openid: - return Response( - {'code': 401, 'message': '支付密码错误', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 7. 查询订单主表(加锁) - with transaction.atomic(): - try: - dingdan_obj = Dingdan.objects.select_related('pingtai_kuozhan').select_for_update().get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response( - {'code': 404, 'message': '订单不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 8. 验证订单状态为1、7、2、8、4(允许退款的状态) - if dingdan_obj.zhuangtai not in [1, 7, 2, 8, 4]: - return Response( - {'code': 400, 'message': '订单状态不允许退款', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 9. 验证订单为平台发单 - if dingdan_obj.fadan_pingtai != 1: - return Response( - {'code': 400, 'message': '此接口只处理平台发单的退款', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 10. 获取订单的重要字段 - jiedan_dashou_id = dingdan_obj.jiedan_dashou_id - jine = dingdan_obj.jine - if jine is None: - return Response( - {'code': 400, 'message': '订单缺少订单金额', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 11. 获取老板ID(从平台订单扩展表) - if not hasattr(dingdan_obj, 'pingtai_kuozhan') or not dingdan_obj.pingtai_kuozhan: - return Response( - {'code': 400, 'message': '订单老板信息不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - laoban_id = dingdan_obj.pingtai_kuozhan.laoban_id - - # 12. 调用微信退款接口(优先使用 wechat_transaction_id) - transaction_id = getattr(dingdan_obj, 'wechat_transaction_id', None) - weixin_refund_result = self.call_weixin_refund( - out_trade_no=dingdan_id, - jine=jine, - transaction_id=transaction_id - ) - - if not weixin_refund_result.get('success'): - logger.error(f"微信退款失败,订单:{dingdan_id},错误:{weixin_refund_result.get('msg')}") - return Response( - {'code': 400, 'message': weixin_refund_result.get('msg', '退款失败')}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 13. 更新订单状态为5(已退款) - dingdan_obj.zhuangtai = 5 - dingdan_obj.save() - - # 14. 更新打手扩展表 - if jiedan_dashou_id: - try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.zhuangtai = 1 # 打手状态设为空闲 - dashou_profile.tuikuanliang += 1 - dashou_profile.save() - except (UserMain.DoesNotExist, AttributeError): - logger.warning(f"平台退款:打手{jiedan_dashou_id}不存在或扩展信息缺失,跳过") - - # 15. 更新老板扩展表 - if laoban_id: - try: - laoban_user = UserMain.objects.get(yonghuid=laoban_id) - laoban_profile = laoban_user.boss_profile - laoban_profile.alltui += 1 - laoban_profile.zonge -= jine - if laoban_profile.zonge < 0: - laoban_profile.zonge = 0 - laoban_profile.save() - except (UserMain.DoesNotExist, AttributeError): - logger.warning(f"平台退款:老板{laoban_id}不存在或扩展信息缺失,跳过") - - # 16. 更新收支记录表(id=1) - try: - - #更新支出记录 - from dingdan.utils import update_daily_payout - update_daily_payout(jine) - - - szjilu_obj = Szjilu.objects.get(id=1) - szjilu_obj.zongsy -= jine - szjilu_obj.zongzc += jine - szjilu_obj.jrzc += jine - szjilu_obj.save() - except Szjilu.DoesNotExist: - logger.error(f"平台退款:收支记录表(id=1)不存在,跳过") - except Exception as e: - logger.error(f"更新收支记录表异常: {str(e)}") - - # 17. 更新退款记录表 - try: - tuikuan_jilu, created = Tuikuanjilu.objects.get_or_create( - dingdan_id=dingdan_id, - defaults={ - 'dashouid': jiedan_dashou_id if jiedan_dashou_id else '', - 'qingqiuid': laoban_id if laoban_id else '', - 'chuliid': qianzhanghao, - 'liyou': tuikuan_liyou if tuikuan_liyou else '给老板退款', - 'sqzhuangtai': 1, - 'jine': jine, - 'dashou_fencheng': dingdan_obj.dashou_fencheng or 0, - 'jieshao': dingdan_obj.jieshao or '', - 'beizhu': '给老板退款', - 'nicheng': dingdan_obj.nicheng or '', - } - ) - if not created: - tuikuan_jilu.sqzhuangtai = 1 - if tuikuan_liyou: - tuikuan_jilu.liyou = tuikuan_liyou - tuikuan_jilu.chuliid = qianzhanghao - tuikuan_jilu.save() - except Exception as e: - logger.error(f"更新退款记录表异常: {str(e)}") - - logger.info(f"平台退款成功,订单:{dingdan_id},退款单号:{weixin_refund_result.get('refund_id')}") - - return Response({'code': 0, 'message': '退款处理成功', 'data': None}) - - 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 call_weixin_refund(self, out_trade_no, jine, transaction_id=None): - """ - 调用微信退款接口 - :param out_trade_no: 商户订单号 - :param jine: 退款金额 - :param transaction_id: 微信支付订单号(优先使用) - :return: {'success': True/False, 'msg': '消息', 'refund_id': '退款单号'} - """ - try: - # 检查配置 - required_settings = ['WEIXIN_APPID', 'WEIXIN_MCHID', 'WEIXIN_SHANGHUMIYAO', 'WEIXIN_CERT_PATH', 'WEIXIN_KEY_PATH'] - for setting in required_settings: - if not hasattr(settings, setting) or not getattr(settings, setting): - return {'success': False, 'msg': f'缺少微信支付配置: {setting}'} - - # 生成退款单号 - refund_id = f"TK{int(time.time())}{random.randint(1000, 9999)}" - total_fee_in_fen = int(float(jine) * 100) - - # 构造请求参数 - refund_data = { - 'appid': settings.WEIXIN_APPID, - 'mch_id': settings.WEIXIN_MCHID, - 'nonce_str': hashlib.md5(str(time.time()).encode()).hexdigest(), - 'out_refund_no': refund_id, - 'total_fee': total_fee_in_fen, - 'refund_fee': total_fee_in_fen, - 'op_user_id': settings.WEIXIN_MCHID, - 'refund_account': 'REFUND_SOURCE_UNSETTLED_FUNDS' - } - # 优先使用 transaction_id,否则用 out_trade_no - if transaction_id: - refund_data['transaction_id'] = transaction_id - else: - refund_data['out_trade_no'] = out_trade_no - - # 生成签名 - sorted_params = sorted(refund_data.items()) - sign_string = '&'.join([f"{k}={v}" for k, v in sorted_params if v]) - sign_string += f"&key={settings.WEIXIN_SHANGHUMIYAO}" - sign = hashlib.md5(sign_string.encode()).hexdigest().upper() - refund_data['sign'] = sign - - xml_data = self.dict_to_xml(refund_data) - url = "https://api.mch.weixin.qq.com/secapi/pay/refund" - response = requests.post( - url, - data=xml_data.encode('utf-8'), - headers={'Content-Type': 'application/xml'}, - cert=(settings.WEIXIN_CERT_PATH, settings.WEIXIN_KEY_PATH), - timeout=30 - ) - - if response.status_code == 200: - result = self.xml_to_dict(response.content) - if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': - logger.info(f"微信退款成功,订单:{out_trade_no},退款单号:{refund_id}") - return {'success': True, 'msg': '退款申请提交成功', 'refund_id': refund_id} - else: - error_msg = result.get('err_code_des', result.get('return_msg', '退款申请失败')) - logger.error(f"微信退款失败,订单:{out_trade_no},错误:{error_msg}") - return {'success': False, 'msg': f'微信退款失败: {error_msg}'} - else: - logger.error(f"微信退款接口调用失败,状态码:{response.status_code}") - return {'success': False, 'msg': '微信退款接口调用失败'} - - except requests.exceptions.Timeout: - logger.error(f"微信退款接口请求超时,订单:{out_trade_no}") - return {'success': False, 'msg': '微信退款接口请求超时'} - except Exception as e: - logger.error(f"调用微信退款接口异常: {str(e)}") - return {'success': False, 'msg': f'退款处理异常: {str(e)}'} - - def dict_to_xml(self, dict_data): - xml = [""] - for k, v in dict_data.items(): - xml.append(f"<{k}>{v}") - xml.append("") - return "".join(xml) - - def xml_to_dict(self, xml_data): - try: - root = ET.fromstring(xml_data) - return {child.tag: child.text for child in root} - except ET.ParseError as e: - logger.error(f"XML解析失败: {str(e)}") - return {'return_code': 'FAIL', 'return_msg': 'XML解析失败'} - - - - -'''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 -from django.conf import settings -import logging -import time -import random -import hashlib -import requests -import xml.etree.ElementTree as ET - -# 获取日志器 -logger = logging.getLogger(__name__) - - - -class AdTongYiTuiKuanPingTai(APIView): - """ - 后台同意退款接口(平台发单) - 权限: JWT Token认证 + 管理员身份验证 + 密码验证 - 前端调用: POST /dingdan/adddpttk - 参数: { - "zhanghao": "管理员账号", - "dingdan_id": "订单ID", - "tuikuan_liyou": "退款理由(可选)", - "pay_password": "支付密码(openid)" - } - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - """ - 同意退款接口(平台发单) - 核心逻辑: - 1. 验证管理员身份和支付密码 - 2. 验证订单状态为2、8或4(进行中、结算中、退款审核中) - 3. 调用微信退款接口 - 4. 如果微信受理成功,更新数据库(订单、打手、老板、收支、退款记录) - 5. 如果微信受理失败,返回错误,数据库不变 - """ - try: - # 1. 获取前端参数 - qianzhanghao = request.data.get('zhanghao', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() - pay_password = request.data.get('pay_password', '').strip() - - if not qianzhanghao or not dingdan_id or not pay_password: - 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. 验证支付密码(对比openid) - if pay_password != dangqianyonghu.openid: - return Response( - {'code': 401, 'message': '支付密码错误', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 7. 查询订单主表 - try: - from dingdan.models import Dingdan, DingdanPingtai, Tuikuanjilu - from peizhi.models import Szjilu - - # 使用select_related预加载平台订单扩展表 - dingdan_obj = Dingdan.objects.select_related('pingtai_kuozhan').get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response( - {'code': 404, 'message': '订单不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 8. 验证订单状态为2、8或4 - if dingdan_obj.zhuangtai not in [1,7,2, 8, 4]: - return Response( - {'code': 400, 'message': '订单状态不允许退款', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 9. 验证订单为平台发单 - if dingdan_obj.fadan_pingtai != 1: - return Response( - {'code': 400, 'message': '此接口只处理平台发单的退款', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 10. 获取订单的重要字段 - jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID - jine = dingdan_obj.jine # 订单金额(全额退款) - - # 11. 验证订单金额是否存在 - if jine is None: - return Response( - {'code': 400, 'message': '订单缺少订单金额', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 12. 获取老板ID(从平台订单扩展表) - try: - pingtai_kuozhan = dingdan_obj.pingtai_kuozhan - laoban_id = pingtai_kuozhan.laoban_id - except DingdanPingtai.DoesNotExist: - return Response( - {'code': 400, 'message': '订单老板信息不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # ========== 修改点开始 ========== - # 13. 先调用微信退款接口,成功后才更新数据库 - weixin_refund_result = self.call_weixin_refund(dingdan_id, jine) - - if not weixin_refund_result.get('success'): - # 微信退款失败,直接返回错误,数据库不更新 - logger.error(f"微信退款失败,订单:{dingdan_id},错误:{weixin_refund_result.get('msg')}") - return Response( - {'code': 400, 'message': weixin_refund_result.get('msg', '退款失败')}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 14. 微信退款受理成功,开始事务更新数据库 - with transaction.atomic(): - # 14.1 更新订单状态为5(已退款) - dingdan_obj.zhuangtai = 5 - dingdan_obj.save() - - # 14.2 更新打手扩展表(退款订单总量+1) - if jiedan_dashou_id: - try: - # 查询打手用户主表 - dashou_user = UserMain.objects.get( - yonghuid=jiedan_dashou_id, - #user_type='dashou' - ) - dashou_profile = dashou_user.dashou_profile - dashou_profile.tuikuanliang += 1 - dashou_profile.save() - except UserMain.DoesNotExist: - logger.warning(f"平台退款:打手{jiedan_dashou_id}不存在,跳过打手更新") - except AttributeError: - logger.warning(f"平台退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手更新") - - # 14.3 更新老板扩展表 - if laoban_id: - try: - laoban_user = UserMain.objects.get( - yonghuid=laoban_id, - #user_type='normal' - ) - laoban_profile = laoban_user.boss_profile - laoban_profile.alltui += 1 - laoban_profile.zonge -= jine - if laoban_profile.zonge < 0: - laoban_profile.zonge = 0 - laoban_profile.save() - except UserMain.DoesNotExist: - logger.warning(f"平台退款:老板{laoban_id}不存在,跳过老板更新") - except AttributeError: - logger.warning(f"平台退款:老板{laoban_id}扩展信息不存在,跳过老板更新") - - # 14.4 更新收支记录表(id=1的记录) - try: - szjilu_obj = Szjilu.objects.get(id=1) - szjilu_obj.zongsy -= jine - szjilu_obj.zongzc += jine - szjilu_obj.jrzc += jine - szjilu_obj.save() - except Szjilu.DoesNotExist: - logger.error(f"平台退款:收支记录表(id=1)不存在,跳过更新") - except Exception as e: - logger.error(f"更新收支记录表异常: {str(e)}") - - # 14.5 更新退款记录表 - try: - # 查询退款记录表,是否已有该订单的记录 - try: - tuikuan_jilu = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) - tuikuan_jilu.sqzhuangtai = 1 - if tuikuan_liyou: - tuikuan_jilu.liyou = tuikuan_liyou - tuikuan_jilu.chuliid = qianzhanghao - tuikuan_jilu.save() - except Tuikuanjilu.DoesNotExist: - # 退款记录表不存在,创建新的记录 - Tuikuanjilu.objects.create( - dingdan_id=dingdan_id, - dashouid=jiedan_dashou_id if jiedan_dashou_id else '', - qingqiuid=laoban_id if laoban_id else '', - chuliid=qianzhanghao, - liyou=tuikuan_liyou if tuikuan_liyou else '给老板退款', - sqzhuangtai=1, - jine=jine, - dashou_fencheng=dingdan_obj.dashou_fencheng if dingdan_obj.dashou_fencheng else 0, - jieshao=dingdan_obj.jieshao if dingdan_obj.jieshao else '', - beizhu='给老板退款', - nicheng=dingdan_obj.nicheng if dingdan_obj.nicheng else '', - ) - except Exception as e: - logger.error(f"更新退款记录表异常: {str(e)}") - - logger.info(f"平台退款成功,订单:{dingdan_id},退款单号:{weixin_refund_result.get('refund_id')}") - # ========== 修改点结束 ========== - - # 15. 返回成功响应 - return Response({ - 'code': 0, - 'message': '退款处理成功', - 'data': None - }) - - 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 call_weixin_refund(self, dingdan_id, jine): - """ - 调用微信退款接口 - 参考你提供的接口,但根据现有模型和配置调整 - - 需要在settings.py中配置以下参数: - WEIXIN_APPID = '你的小程序appid' - WEIXIN_MCHID = '你的商户号' - WEIXIN_SHANGHUMIYAO = '你的商户密钥' - WEIXIN_CERT_PATH = '证书路径/cert/apiclient_cert.pem' - WEIXIN_KEY_PATH = '证书路径/cert/apiclient_key.pem' - - 返回:{'success': True/False, 'msg': '消息'} - """ - try: - # 检查必要的配置 - required_settings = ['WEIXIN_APPID', 'WEIXIN_MCHID', 'WEIXIN_SHANGHUMIYAO', 'WEIXIN_CERT_PATH', - 'WEIXIN_KEY_PATH'] - for setting in required_settings: - if not hasattr(settings, setting) or not getattr(settings, setting): - return {'success': False, 'msg': f'缺少微信支付配置: {setting}'} - - # 生成退款单号 - refund_id = f"TK{int(time.time())}{random.randint(1000, 9999)}" - - # 金额转换为分(必须是整数) - total_fee_in_fen = int(float(jine) * 100) - - # 准备微信退款参数 - refund_data = { - 'appid': settings.WEIXIN_APPID, - 'mch_id': settings.WEIXIN_MCHID, - 'nonce_str': hashlib.md5(str(time.time()).encode()).hexdigest(), - 'out_trade_no': dingdan_id, # 商户订单号(我们的订单ID) - 'out_refund_no': refund_id, # 商户退款单号 - 'total_fee': total_fee_in_fen, # 订单总金额(分) - 'refund_fee': total_fee_in_fen, # 退款金额(分),全额退款 - 'op_user_id': settings.WEIXIN_MCHID, # 操作员ID,默认为商户号 - 'refund_account': 'REFUND_SOURCE_UNSETTLED_FUNDS' # 退款资金来源 - } - - # 生成签名 - sorted_params = sorted(refund_data.items()) - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f"&key={settings.WEIXIN_SHANGHUMIYAO}" - sign = hashlib.md5(sign_string.encode()).hexdigest().upper() - refund_data['sign'] = sign - - # 将字典转换为XML - xml_data = self.dict_to_xml(refund_data) - - # 微信退款API地址 - weixin_refund_url = "https://api.mch.weixin.qq.com/secapi/pay/refund" - - # 设置超时时间 - timeout = 30 - - # 调用微信退款API(需要证书) - headers = {'Content-Type': 'application/xml'} - response = requests.post( - weixin_refund_url, - data=xml_data.encode('utf-8'), - headers=headers, - cert=(settings.WEIXIN_CERT_PATH, settings.WEIXIN_KEY_PATH), - timeout=timeout - ) - - # 解析返回结果 - if response.status_code == 200: - result = self.xml_to_dict(response.content) - - if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': - logger.info(f"微信退款成功,订单:{dingdan_id},退款单号:{refund_id}") - return {'success': True, 'msg': '退款申请提交成功', 'refund_id': refund_id} - else: - error_msg = result.get('err_code_des', result.get('return_msg', '退款申请失败')) - logger.error(f"微信退款失败,订单:{dingdan_id},错误:{error_msg}") - return {'success': False, 'msg': f'微信退款失败: {error_msg}'} - else: - logger.error(f"微信退款接口调用失败,状态码:{response.status_code}") - return {'success': False, 'msg': '微信退款接口调用失败'} - - except requests.exceptions.Timeout: - logger.error(f"微信退款接口请求超时,订单:{dingdan_id}") - return {'success': False, 'msg': '微信退款接口请求超时'} - except Exception as e: - logger.error(f"调用微信退款接口异常: {str(e)}") - return {'success': False, 'msg': f'退款处理异常: {str(e)}'} - - - - def dict_to_xml(self, dict_data): - """将字典转换为XML字符串""" - xml = [""] - for k, v in dict_data.items(): - xml.append(f"<{k}>{v}") - xml.append("") - return "".join(xml) - - def xml_to_dict(self, xml_data): - """将XML转换为字典""" - try: - root = ET.fromstring(xml_data) - result = {} - for child in root: - result[child.tag] = child.text - return result - except ET.ParseError as e: - logger.error(f"XML解析失败: {str(e)}") - return {'return_code': 'FAIL', 'return_msg': 'XML解析失败'}''' - - - - - - - - - - - - - - - - -logger = logging.getLogger(__name__) - - -class DashouHuoquLeixingView(APIView): - """ - 打手获取商品类型接口 - 抢单页面使用 - 接口地址:POST /dingdan/dsqdhqddlx - 功能:返回所有正常的商品类型,已按排序权重排序 - - 返回格式(与前端原数据格式完全一致): - { - "code": 200, - "msg": "成功", - "data": { - "list": [ - { - "id": 1, - "jieshao": "王者荣耀", - "tupian_url": "leixing/wzry.jpg", - "yaoqiuleixing": null, - "huiyuan_id": null, - "yongjin": null, - "paixu": 100, - "shenhezhuangtai": 1 - }, - // ... 更多类型 - ] - } - } - """ - - # 🔴 使用JWT Token认证 - permission_classes = [IsAuthenticated] - - def post(self, request): - """ - 处理POST请求,返回商品类型列表 - - 请求参数:无特殊参数,只验证用户身份 - - 安全验证: - 1. JWT Token验证用户身份 - 2. 只返回 shenhezhuangtai=1(正常状态)的类型 - 3. 数据已排序:paixu降序 → id升序 - """ - try: - logger.info(f"打手获取商品类型请求 - 用户ID: {request.user.yonghuid}") - - # 🔴 导入商品类型模型 - from shangpin.models import ShangpinLeixing - - # 🔴 查询所有正常状态的商品类型 - # 条件:shenhezhuangtai=1(正常状态) - # 排序:先按 paixu 降序(数值大的在前),再按 id 升序 - leixing_queryset = ShangpinLeixing.objects.filter( - shenhezhuangtai=1 # 🔴 只取正常状态,排除审核专用 - ).order_by('-paixu', 'id') - - # 🔴 构建返回数据列表 - leixing_list = [] - for leixing in leixing_queryset: - leixing_list.append({ - "id": leixing.id, - "jieshao": leixing.jieshao or "", - "tupian_url": leixing.tupian_url or "", - "yaoqiuleixing": leixing.yaoqiuleixing, - "huiyuan_id": leixing.huiyuan_id, - "yongjin": float(leixing.yongjin) if leixing.yongjin else None, - "paixu": leixing.paixu or 0, - "shenhezhuangtai": leixing.shenhezhuangtai or 1 - }) - - # 🔴 记录日志 - logger.info(f"返回商品类型数据:{len(leixing_list)}条,用户: {request.user.yonghuid}") - - # 🔴 返回成功响应 - # 注意:保持与前端原数据格式完全一致 - return Response({ - "code": 200, - "msg": "成功", - "data": { - "list": leixing_list # 🔴 保持原字段名 "list" - } - }, 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": f"服务器内部错误: {str(e)}", - "data": None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - - - - - - - - -logger = logging.getLogger(__name__) - -from peizhi.models import Club ,OrderTypeMapping - -logger = logging.getLogger(__name__) - - - - -class PartnerSyncOrderView(APIView): - """ - 接收对方平台派单通知接口 - 对方平台商家派单成功后,异步调用此接口,将订单信息同步过来。 - """ - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 验证 token - token = request.data.get('token') - if token != settings.CROSS_PLATFORM_TOKEN: - logger.warning(f"跨平台派单 token 无效: {token}") - return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) - - # 2. 获取必要参数 - our_club_id = request.data.get('our_club_id') # 对方平台发送的我方俱乐部ID(即我方在对方表中的 partner_club_id) - partner_club_id = request.data.get('partner_club_id') # 对方平台的俱乐部ID(对方自己的ID) - partner_order_id = request.data.get('partner_order_id') - jine = request.data.get('jine') - dashou_fencheng = request.data.get('dashou_fencheng') - jieshao = request.data.get('jieshao') - beizhu = request.data.get('beizhu', '') - create_time_str = request.data.get('create_time') - nicheng = request.data.get('nicheng') - leixing_id = request.data.get('leixing_id') # 对方平台的商品类型ID - partner_yonghu_id = request.data.get('user_id') # 对方下单用户ID - - # 必需字段校验 - required_fields = [our_club_id, partner_club_id, partner_order_id, jine, dashou_fencheng, - jieshao, create_time_str, nicheng, leixing_id] - if not all(required_fields): - logger.error(f"跨平台派单参数缺失: {request.data}") - return Response({'code': 400, 'message': 'Missing required parameters'}, status=status.HTTP_400_BAD_REQUEST) - - # 3. 验证我方俱乐部是否存在且同意接收订单 - try: - my_club = Club.objects.get(partner_club_id=our_club_id) - except Club.DoesNotExist: - logger.warning(f"我方俱乐部不存在,partner_club_id={our_club_id}") - return Response({'code': 404, 'message': 'Club not found'}, status=status.HTTP_404_NOT_FOUND) - - if not my_club.is_receive_enabled: - logger.warning(f"我方俱乐部 {my_club.club_id} 未同意接收订单") - return Response({'code': 403, 'message': 'Receiving not allowed'}, status=status.HTTP_403_FORBIDDEN) - - # 4. 查找订单类型映射 - try: - mapping = OrderTypeMapping.objects.get( - partner_club_id=our_club_id, # 对方在我方的标识 - partner_type_id=leixing_id - ) - our_type_id = mapping.our_type_id - except OrderTypeMapping.DoesNotExist: - logger.error(f"未找到订单类型映射: 对方俱乐部={our_club_id}, 对方类型ID={leixing_id}") - return Response({'code': 400, 'message': 'Order type mapping not found'}, status=status.HTTP_400_BAD_REQUEST) - - # 5. 获取我方商品类型信息(用于获取会员ID、佣金等) - try: - our_leixing = ShangpinLeixing.objects.get(id=our_type_id) - except ShangpinLeixing.DoesNotExist: - logger.error(f"我方商品类型不存在: {our_type_id}") - return Response({'code': 400, 'message': 'Local product type not found'}, status=status.HTTP_400_BAD_REQUEST) - - # 6. 幂等检查:如果已存在相同 partner_order_id 的订单,直接返回(附带 status='existed') - existing = Dingdan.objects.filter(partner_order_id=partner_order_id).first() - if existing: - logger.info(f"跨平台派单幂等返回: partner_order_id={partner_order_id} 已存在") - return Response({ - 'code': 0, - 'data': { - 'local_order_id': existing.dingdan_id, - 'status': 'existed' # 明确告知已存在 - } - }) - - # 7. 生成我方订单ID(确保唯一性) - 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}" - - # 8. 获取代理商家头像(用于订单图片) - agent_user = UserMain.objects.filter(yonghuid=settings.AGENT_SHANGJIA_ID).first() - agent_avatar = agent_user.avatar if agent_user else '' - - # 9. 计算我方实际打手分成金额:对方传来的打手分成 × 我方接收费率 - try: - actual_dashou_fencheng = Decimal(dashou_fencheng) * my_club.receive_rate - except Exception: - actual_dashou_fencheng = Decimal(dashou_fencheng) - - # 10. 事务创建订单 - try: - with transaction.atomic(): - new_id = generate_dingdan_id() - # 创建主订单 - dingdan = Dingdan.objects.create( - dingdan_id=new_id, - zhuangtai=1, # 普通订单(待接单) - fadan_pingtai=2, # 商家发单 - jine=jine, # 原始订单金额 - dashou_fencheng=actual_dashou_fencheng, # 调整后的打手分成 - jieshao=jieshao, - beizhu=beizhu, - create_time=create_time_str, - yaoqiuleixing=our_leixing.yaoqiuleixing, - nicheng=nicheng, - huiyuan_id=our_leixing.huiyuan_id, - yongjin=our_leixing.yongjin, - leixing_id=our_type_id, - partner_order_id=partner_order_id, # 存储对方订单ID - is_cross=1, - dispatch_type=2, # 对方派单 - partner_club_id=our_club_id, # 对方俱乐部ID - tupian=agent_avatar, # 使用代理商家头像作为订单图片 - ) - # 创建商家扩展表(使用代理商家) - DingdanShangjia.objects.create( - dingdan=dingdan, - shangjia_id=settings.AGENT_SHANGJIA_ID, - sjnicheng='跨平台代理', - partner_yonghu_id=partner_yonghu_id or '', - ) - - # 成功创建后更新每日派单统计(方向=对方派单) - try: - update_daily_dispatch_stat( - direction=2, # 对方派单 - partner_club_id=our_club_id, - dispatch_amount=Decimal(jine), # 统计使用原始订单金额 - - ) - except Exception as e: - logger.error(f"更新派单统计失败: {e}", exc_info=True) - - 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 = { - '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) # 瞬间返回,不阻塞请求 - - - - - return Response({ - 'code': 0, - 'data': { - 'local_order_id': new_id, - 'status': 'created' - } - }) - - except Exception as e: - logger.error(f"跨平台订单同步失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'message': 'Internal error'}, 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 '游戏订单' - - - - -from dingdan.utils import update_daily_dispatch_stat - -logger = logging.getLogger(__name__) - - - - - - -class PartnerCheckClaimView(APIView): - """ - 接收对方平台抢单询问(对方打手抢我方派出的订单) - 如果允许抢单: - 1. 订单状态改为2(已接单) - 2. 记录对方打手ID、对方俱乐部ID、对方订单ID - 3. 更新派单成交统计(success_count/success_amount) - """ - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - token = request.data.get('token') - if token != settings.CROSS_PLATFORM_TOKEN: - logger.warning(f"token无效: {token}") - return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) - - wofangdingdan_id = request.data.get('wofangdingdan_id') - partner_order_id = request.data.get('partner_order_id') - our_club_id = request.data.get('our_club_id') - dashou_id = request.data.get('dashou_id') - sj_id = request.data.get('sj_id') - - if not all([wofangdingdan_id, partner_order_id, our_club_id, dashou_id, sj_id]): - logger.error(f"参数缺失: {request.data}") - return Response({'code': 400, 'message': 'Missing parameters'}, status=status.HTTP_400_BAD_REQUEST) - - try: - with transaction.atomic(): - # 查询我方俱乐部配置 - try: - club = Club.objects.select_for_update().get(partner_club_id=our_club_id) - except Club.DoesNotExist: - logger.warning(f"俱乐部配置不存在 partner_club_id={our_club_id}") - return Response({'code': 0, 'data': {'can_claim': False}}) - - if not club.is_interop_enabled: - logger.warning(f"俱乐部 {club.club_id} 未开启互通") - return Response({'code': 0, 'data': {'can_claim': False}}) - - # 锁定订单 - try: - order = Dingdan.objects.select_for_update().get(dingdan_id=wofangdingdan_id) - except Dingdan.DoesNotExist: - logger.warning(f"订单不存在: {wofangdingdan_id}") - return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND) - - # 只有状态1可抢 - if order.zhuangtai != 1: - logger.info(f"订单 {order.dingdan_id} 状态 {order.zhuangtai} 不可抢") - return Response({'code': 0, 'data': {'can_claim': False}}) - - # 商品类型检查 - try: - leixing = ShangpinLeixing.objects.get(id=order.leixing_id) - except ShangpinLeixing.DoesNotExist: - logger.warning(f"商品类型不存在: {order.leixing_id}") - return Response({'code': 0, 'data': {'can_claim': False}}) - - if not leixing.is_cross_enabled: - logger.warning(f"商品类型 {leixing.jieshao} 未开启跨平台") - return Response({'code': 0, 'data': {'can_claim': False}}) - - # 执行抢单:状态改为2(已接单) - order.zhuangtai = 2 - order.jiedan_dashou_id = dashou_id - order.partner_club_id = our_club_id - order.partner_order_id = partner_order_id - if order.is_cross != 1: - order.is_cross = 1 - # dispatch_type 保持原值(1-我方派单),不变 - order.save() - - # 更新派单成交统计(我方派单被成功接单) - update_daily_dispatch_stat( - direction=1, - partner_club_id=our_club_id, - claimed_amount=order.jine, - - ) - - logger.info(f"对方抢单成功: 订单 {order.dingdan_id}, 对方打手 {dashou_id}") - return Response({'code': 0, 'data': {'can_claim': True}}) - - except Exception as e: - logger.error(f"异常: {e}", exc_info=True) - return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - -class PartnerNotifyClaimView(APIView): - """ - 接收对方平台抢单成功通知(对方派单被他们自己的打手抢了) - 我方根据 partner_order_id 找到订单,如果状态为1则改为20,不更新任何统计。 - """ - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - token = request.data.get('token') - if token != settings.CROSS_PLATFORM_TOKEN: - logger.warning(f"token无效: {token}") - return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) - - partner_order_id = request.data.get('partner_order_id') - if not partner_order_id: - logger.error("缺少 partner_order_id") - return Response({'code': 400, 'message': 'Missing partner_order_id'}, status=status.HTTP_400_BAD_REQUEST) - - try: - with transaction.atomic(): - order = Dingdan.objects.select_for_update().get(partner_order_id=partner_order_id) - # 只有状态为1(未被抢)时才改为20 - if order.zhuangtai == 1: - order.zhuangtai = 20 - order.save() - logger.info(f"订单 {order.dingdan_id} 状态改为20(被其他俱乐部抢走)") - else: - logger.info(f"订单 {order.dingdan_id} 当前状态 {order.zhuangtai},无需修改") - return Response({'code': 0, 'message': 'OK'}) - except Dingdan.DoesNotExist: - logger.warning(f"订单不存在 partner_order_id={partner_order_id}") - return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - logger.error(f"异常: {e}", exc_info=True) - return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - -'''class PartnerSyncOrderView(APIView): - """ - 接收对方平台派单通知接口 - 对方平台商家派单成功后,异步调用此接口,将订单信息同步过来。 - 请求方式: POST - 接口地址: /dingdan/partner_sync_order - """ - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 验证 token - token = request.data.get('token') - if token != settings.CROSS_PLATFORM_TOKEN: - logger.warning(f"跨平台派单 token 无效: {token}") - return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) - - # 2. 获取必要参数 - our_club_id = request.data.get('our_club_id') # 对方平台发送的我方俱乐部ID(即对方在 Club 表中 partner_club_id) - partner_club_id = request.data.get('partner_club_id') # 对方平台的俱乐部ID(对方自己的ID) - partner_order_id = request.data.get('partner_order_id') - jine = request.data.get('jine') - dashou_fencheng = request.data.get('dashou_fencheng') - jieshao = request.data.get('jieshao') - beizhu = request.data.get('beizhu', '') - create_time_str = request.data.get('create_time') - nicheng = request.data.get('nicheng') - leixing_id = request.data.get('leixing_id') # 对方平台的商品类型ID - - # 必需字段校验 - required_fields = [our_club_id, partner_club_id, partner_order_id, jine, dashou_fencheng, - jieshao, create_time_str, nicheng, leixing_id] - if not all(required_fields): - logger.error(f"跨平台派单参数缺失: {request.data}") - return Response({'code': 400, 'message': 'Missing required parameters'}, status=status.HTTP_400_BAD_REQUEST) - - # 3. 验证我方俱乐部是否存在且同意接收订单 - # 注意:our_club_id 是对方发送过来的,代表我方在对方系统中的ID,所以用 partner_club_id 匹配 - try: - my_club = Club.objects.get(partner_club_id=our_club_id) - except Club.DoesNotExist: - logger.warning(f"我方俱乐部不存在,partner_club_id={our_club_id}") - return Response({'code': 404, 'message': 'Club not found'}, status=status.HTTP_404_NOT_FOUND) - - if not my_club.is_receive_enabled: - logger.warning(f"我方俱乐部 {my_club.club_id} 未同意接收订单") - return Response({'code': 403, 'message': 'Receiving not allowed'}, status=status.HTTP_403_FORBIDDEN) - - # 4. 查找订单类型映射 - try: - mapping = OrderTypeMapping.objects.get( - partner_club_id=our_club_id, - partner_type_id=leixing_id - ) - our_type_id = mapping.our_type_id - except OrderTypeMapping.DoesNotExist: - logger.error(f"未找到订单类型映射: 对方俱乐部={our_club_id}, 对方类型ID={leixing_id}") - return Response({'code': 400, 'message': 'Order type mapping not found'}, status=status.HTTP_400_BAD_REQUEST) - - # 5. 获取我方商品类型信息(用于获取会员ID、佣金等) - try: - our_leixing = ShangpinLeixing.objects.get(id=our_type_id) - except ShangpinLeixing.DoesNotExist: - logger.error(f"我方商品类型不存在: {our_type_id}") - return Response({'code': 400, 'message': 'Local product type not found'}, status=status.HTTP_400_BAD_REQUEST) - - # 6. 幂等检查:如果已存在相同 partner_order_id 的订单,直接返回 - existing = Dingdan.objects.filter(partner_order_id=partner_order_id).first() - if existing: - logger.info(f"跨平台派单幂等返回: partner_order_id={partner_order_id} 已存在") - return Response({ - 'code': 0, - 'data': {'local_order_id': existing.dingdan_id} - }) - - # 7. 生成我方订单ID - def generate_dingdan_id(): - import time, random, os - 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}" - - # 8. 获取代理商家头像(用于订单图片) - agent_user = UserMain.objects.filter(yonghuid=settings.AGENT_SHANGJIA_ID).first() - agent_avatar = agent_user.avatar if agent_user else '' - - # 9. 事务创建订单 - try: - with transaction.atomic(): - new_id = generate_dingdan_id() - # 创建主订单 - dingdan = Dingdan.objects.create( - dingdan_id=new_id, - zhuangtai=1, # 普通订单(默认) - fadan_pingtai=2, # 商家发单 - jine=jine, - dashou_fencheng=dashou_fencheng, - jieshao=jieshao, - beizhu=beizhu, - create_time=create_time_str, - yaoqiuleixing=our_leixing.yaoqiuleixing, - nicheng=nicheng, - huiyuan_id=our_leixing.huiyuan_id, - yongjin=our_leixing.yongjin, - leixing_id=our_type_id, - partner_order_id=partner_order_id, - is_cross=1, - dispatch_type=2, # 对方派给我方 - partner_club_id=our_club_id, # 对方的俱乐部ID - tupian=agent_avatar, # 使用代理商家头像作为订单图片 - ) - # 创建商家扩展表(使用代理商家) - DingdanShangjia.objects.create( - dingdan=dingdan, - shangjia_id=settings.AGENT_SHANGJIA_ID, - sjnicheng='跨平台代理' - ) - logger.info(f"跨平台订单同步成功: partner_order_id={partner_order_id}, local_id={new_id}") - return Response({ - 'code': 0, - 'data': {'local_order_id': new_id} - },status = 200) - except Exception as e: - logger.error(f"跨平台订单同步失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -from dingdan.utils import update_daily_dispatch_stat - -logger = logging.getLogger(__name__) - - - - - - -class PartnerCheckClaimView(APIView): - """ - 接收对方平台抢单询问接口 - 对方平台打手抢单前,调用此接口询问订单是否可抢。 - 请求方式: POST - 接口地址: /dingdan/partner_check_claim - 请求参数: - token: 认证令牌 - partner_order_id: 对方平台订单ID(即我方记录的 partner_order_id) - partner_club_id: 对方俱乐部ID(对方在我方系统中的标识) - dashou_id: 对方平台打手ID - sj_id: 对方平台商家ID(带前缀,如 SjAGENT001) - 返回: - code: 0成功 - data: { - can_claim: true/false - } - 注意:此接口会原子地检查并更新订单状态,必须使用行锁。 - """ - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 验证 token - token = request.data.get('token') - if token != settings.CROSS_PLATFORM_TOKEN: - logger.warning(f"跨平台抢单询问 token 无效: {token}") - return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) - - # 2. 获取必要参数 - duifangdingdan_id = request.data.get('wofangdingdan_id') - partner_order_id = request.data.get('partner_order_id') # 我方订单ID - partner_club_id = request.data.get('our_club_id') # 对方俱乐部ID - dashou_id = request.data.get('dashou_id') # 对方打手ID - sj_id = request.data.get('sj_id') # 对方商家ID(带前缀,如 SjAGENT001) - - if not all([partner_order_id, partner_club_id, dashou_id, sj_id]): - logger.error(f"跨平台抢单询问参数缺失: {request.data}") - return Response({'code': 400, 'message': 'Missing parameters'}, status=status.HTTP_400_BAD_REQUEST) - - try: - with transaction.atomic(): - # 3. 查询我方俱乐部配置(通过对方的 partner_club_id) - try: - club = Club.objects.select_for_update().get(partner_club_id=partner_club_id) - except Club.DoesNotExist: - logger.warning(f"跨平台抢单询问: 我方俱乐部配置不存在 partner_club_id={partner_club_id}") - return Response({'code': 404, 'message': 'Club not found'}, status=status.HTTP_404_NOT_FOUND) - - # 检查俱乐部是否已开启平台互通(我方同意接收对方订单) - if not club.is_interop_enabled: - logger.warning(f"俱乐部 {club.club_id} 未开启接收跨平台订单,拒绝抢单") - return Response({'code': 0, 'data': {'can_claim': False}}) - - # 4. 查询订单并锁定 - try: - order = Dingdan.objects.select_for_update().get(dingdan_id=partner_order_id) - except Dingdan.DoesNotExist: - logger.warning(f"跨平台抢单询问: 订单不存在 dingdan_id={partner_order_id}") - return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND) - - # 5. 检查订单状态(必须为1) - if order.zhuangtai != 1: - logger.info(f"跨平台抢单失败: 订单状态 {order.zhuangtai},不可抢") - return Response({'code': 0, 'data': {'can_claim': False}}) - - # 6. 检查订单对应的商品类型是否支持跨平台抢单 - try: - leixing = ShangpinLeixing.objects.get(id=order.leixing_id) - except ShangpinLeixing.DoesNotExist: - logger.warning(f"订单 {order.dingdan_id} 的商品类型 {order.leixing_id} 不存在") - return Response({'code': 0, 'data': {'can_claim': False}}) - - if not leixing.is_cross_enabled: - logger.warning(f"商品类型 {leixing.jieshao} 未开启跨平台抢单") - return Response({'code': 0, 'data': {'can_claim': False}}) - - # 7. 所有检查通过,执行抢单操作 - # 更新订单主表 - order.zhuangtai = 2 # 已接单 - order.jiedan_dashou_id = dashou_id # 对方打手ID - order.is_cross = 1 # 标记为跨平台订单 - order.dispatch_type = 1 # 我方派单,对方抢单 - order.partner_club_id = partner_club_id # 记录对方俱乐部ID - order.partner_order_id = duifangdingdan_id#记录对方订单id - # 存储对方商家ID(带前缀)到商家扩展表的 shangjia_id(如果原来有则更新) - # 注意:商家扩展表已存在,需要更新或创建?原订单可能已有 shangjia_kuozhan,我们直接更新 - order.save() - - - - # 8. 记录每日派单统计(使用新统计函数,不再更新 Club 表) - fencheng = order.dashou_fencheng or Decimal('0.00') - amount = fencheng * club.dashou_rate - update_daily_dispatch_stat( - dispatch_date=timezone.now().date(), - direction=2, # 对方派单 - partner_club_id=partner_club_id, - amount=amount, - count=1 - ) - # 9. 返回成功,告知对方可以抢单 - logger.info(f"跨平台抢单成功: 订单 {order.dingdan_id}, 对方打手 {dashou_id}, 俱乐部 {club.club_id}") - return Response({'code': 0, 'data': {'can_claim': True}}) - - except Exception as e: - logger.error(f"跨平台抢单询问异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - - - - -class PartnerNotifyClaimView(APIView): - """ - 接收对方平台抢单成功通知接口 - 对方平台打手抢单成功后,调用此接口,我方处理本地订单。 - 请求方式: POST - 接口地址: /dingdan/partner_notify_claim - 请求参数: - token: 认证令牌,需与settings.CROSS_PLATFORM_TOKEN一致 - order_id: 对方平台的订单ID(即我方记录的 partner_order_id) - 返回: - code: 0成功,其他失败 - message: 描述信息 - """ - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 验证token - token = request.data.get('token') - if token != settings.CROSS_PLATFORM_TOKEN: - logger.warning(f"跨平台抢单通知 token 无效: {token}") - return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) - - order_id = request.data.get('local_order_id') # 对方平台的订单ID(即我方的 partner_order_id) - if not order_id: - logger.error("跨平台抢单通知参数缺失: order_id") - return Response({'code': 400, 'message': 'Missing order_id'}, status=status.HTTP_400_BAD_REQUEST) - - try: - with transaction.atomic(): - # 2. 根据 partner_order_id 查找订单 - order = Dingdan.objects.select_for_update().get(partner_order_id=order_id) - - # 3. 根据订单状态处理 - if order.zhuangtai == 1: - # 订单未被抢,直接删除(包括关联的商家扩展表) - # 先删除扩展表 - DingdanShangjia.objects.filter(dingdan=order).delete() - # 再删除主订单 - order.delete() - logger.info(f"跨平台抢单通知:订单状态为1,已删除,partner_order_id={order_id}") - return Response({'code': 0, 'message': 'Order deleted'}) - else: - # 订单状态不是1(已被抢或异常),改为状态15(异常订单) - order.zhuangtai = 15 - order.save() - logger.info(f"跨平台抢单通知:订单状态异常,已改为15,partner_order_id={order_id}") - return Response({'code': 0, 'message': 'Order marked as abnormal'}) - - except Dingdan.DoesNotExist: - logger.warning(f"跨平台抢单通知:订单不存在 partner_order_id={order_id}") - return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - logger.error(f"跨平台抢单通知异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' - - - - - - - - - -class PartnerRefundNotifyView(APIView): - """ - 接收对方平台退款通知接口 - 对方平台商家申请退款时,调用此接口通知我方,我方将本地对应订单改为退款状态。 - 请求方式: POST - 接口地址: /dingdan/partner_refund_notify - 请求参数: - token: 认证令牌,需与settings.CROSS_PLATFORM_TOKEN一致 - order_id: 对方平台的订单ID(即我方记录的 partner_order_id) - 返回: - code: 0成功,其他失败 - message: 描述信息 - """ - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 验证token - token = request.data.get('token') - if token != settings.CROSS_PLATFORM_TOKEN: - logger.warning(f"跨平台退款通知 token 无效: {token}") - return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) - - order_id = request.data.get('order_id') # 对方平台的订单ID(即我方的 partner_order_id) - if not order_id: - logger.error("跨平台退款通知参数缺失: order_id") - return Response({'code': 400, 'message': 'Missing order_id'}, status=status.HTTP_400_BAD_REQUEST) - - try: - with transaction.atomic(): - # 2. 根据 partner_order_id 查找订单(必须存在且为跨平台订单) - order = Dingdan.objects.select_for_update().get(partner_order_id=order_id, is_cross=1) - # 如果订单已完成,不允许退款 - if order.zhuangtai == 3: - logger.warning(f"跨平台退款通知:订单 {order_id} 已完成,不可退款") - return Response({'code': 400, 'message': 'Order already completed, cannot refund'}, status=status.HTTP_400_BAD_REQUEST) - - # 3. 如果订单状态不是5,则改为5(已退款),并更新打手信息 - if order.zhuangtai != 5: - # 记录原打手ID - old_dashou_id = order.jiedan_dashou_id - if old_dashou_id: - try: - # 查询打手用户主表 - dashou_user = UserMain.objects.get(yonghuid=old_dashou_id) - # 通过反向关系获取打手扩展表 - if hasattr(dashou_user, 'dashou_profile'): - dashou_profile = dashou_user.dashou_profile - dashou_profile.zhuangtai = 1 # 恢复打手状态 - dashou_profile.tuikuanliang += 1 # 退款订单量+1 - dashou_profile.save() - logger.info(f"退款通知:打手 {old_dashou_id} 状态已恢复,退款订单+1") - except UserMain.DoesNotExist: - logger.warning(f"退款通知:打手用户不存在 {old_dashou_id}") - except Exception as e: - logger.error(f"退款通知:更新打手信息失败 {str(e)}") - - # 更新订单状态为5 - order.zhuangtai = 5 - # 可选:清空接单打手ID - #order.jiedan_dashou_id = None - order.save() - logger.info(f"跨平台退款通知成功:订单 {order_id} 已改为退款状态") - else: - logger.info(f"跨平台退款通知:订单 {order_id} 已是退款状态,无需重复处理") - - return Response({'code': 0, 'message': 'Refund processed successfully'}) - - except Dingdan.DoesNotExist: - logger.warning(f"跨平台退款通知:订单不存在或不是跨平台订单,partner_order_id={order_id}") - return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - logger.error(f"跨平台退款通知异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - - -logger = logging.getLogger(__name__) - - -class AdJuJueJieSuan(APIView): - """ - 后台拒绝结算接口(平台发单) - 权限: JWT Token认证 + 管理员身份验证 - 前端调用: POST /dingdan/adddjjjs - 参数: { - "zhanghao": "管理员账号(手机号)", - "dingdan_id": "订单ID", - "tkly": "拒绝结算理由(必填)" - } - """ - 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() - tkly = request.data.get('tkly', '').strip() - - if not all([qianzhanghao, dingdan_id, tkly]): - return Response( - {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id, tkly)', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2. JWT用户验证 - dangqianyonghu = request.user - - # 3. 验证手机号匹配(管理员账号通常为手机号) - if 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: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response( - {'code': 404, 'message': '订单不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 6. 校验订单条件:平台发单(1)且状态为2或8 - if dingdan.fadan_pingtai != 1 or dingdan.zhuangtai not in [2, 8]: - return Response( - {'code': 400, 'message': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - jiedan_dashou_id = dingdan.jiedan_dashou_id - if not jiedan_dashou_id: - return Response( - {'code': 400, 'message': '订单暂无接单打手', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - with transaction.atomic(): - # 7. 更新订单状态为5(已退款),并保存拒绝理由 - dingdan.zhuangtai = 5 - dingdan.tkly = tkly - dingdan.save() - - # 8. 更新打手扩展表 - 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() - except (UserMain.DoesNotExist, AttributeError) as e: - logger.warning(f"拒绝结算:打手{jiedan_dashou_id}信息更新失败 - {str(e)}") - - return Response({'code': 0, 'message': '拒绝结算成功', 'data': None}) - - 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 AdZhuanYiDaTing(APIView): - """ - 后台转移大厅接口 - 权限: JWT Token认证 + 管理员身份验证 - 前端调用: POST /dingdan/adddzydt - 参数: { - "zhanghao": "管理员账号(手机号)", - "dingdan_id": "订单ID" - } - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - try: - qianzhanghao = request.data.get('zhanghao', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - - if not all([qianzhanghao, dingdan_id]): - return Response( - {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id)', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - dangqianyonghu = request.user - - if 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: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response( - {'code': 404, 'message': '订单不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 校验订单状态:允许2、8、4 - if dingdan.zhuangtai not in [2, 8, 4]: - return Response( - {'code': 400, 'message': '仅状态为进行中(2)、结算中(8)或退款审核(4)的订单可转移大厅', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - jiedan_dashou_id = dingdan.jiedan_dashou_id - - with transaction.atomic(): - # 更新订单:状态改为1(下单中),清空接单打手ID - dingdan.zhuangtai = 1 - dingdan.jiedan_dashou_id = None - dingdan.save() - - # 如果有接单打手,更新其扩展表 - if jiedan_dashou_id: - try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.zhuangtai = 1 - dashou_profile.tuikuanliang += 1 - dashou_profile.save() - except (UserMain.DoesNotExist, AttributeError) as e: - logger.warning(f"转移大厅:打手{jiedan_dashou_id}信息更新失败 - {str(e)}") - - return Response({'code': 0, 'message': '转移大厅成功', 'data': None}) - - 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 AdQuXiaoZhiDing(APIView): - """ - 后台取消指定接口 - 权限: JWT Token认证 + 管理员身份验证 - 前端调用: POST /dingdan/adddqxzd - 参数: { - "zhanghao": "管理员账号(手机号)", - "dingdan_id": "订单ID" - } - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - try: - qianzhanghao = request.data.get('zhanghao', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - - if not all([qianzhanghao, dingdan_id]): - return Response( - {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id)', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - dangqianyonghu = request.user - - if 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: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response( - {'code': 404, 'message': '订单不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 校验订单状态:必须为7(指定中) - if dingdan.zhuangtai != 7: - return Response( - {'code': 400, 'message': '仅状态为指定中(7)的订单可取消指定', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - with transaction.atomic(): - # 更新订单:状态改为1(下单中),清空指定ID - dingdan.zhuangtai = 1 - dingdan.zhiding_id = None - dingdan.save() - - return Response({'code': 0, 'message': '取消指定成功', 'data': None}) - - 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 DashouXiugaiView(APIView): - """ - 打手修改订单交付内容接口 - 请求方式:POST - 请求路径:/dingdan/dsxiugaidd - 权限:需携带有效JWT Token,且用户为打手身份 - 请求参数: - dingdan_id : 订单ID(必填) - liuyan : 新的留言内容(可选,最长50字) - deleted_images : 要删除的图片URL列表(数组,每个元素为完整COS URL) - new_images : 新增的图片URL列表(数组,每个元素为完整COS URL) - 返回: - 成功:{"code":0,"msg":"修改成功","data":null} - 失败:返回相应错误码和提示 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 获取请求数据 - data = request.data - dingdan_id = data.get('dingdan_id', '') - liuyan = data.get('liuyan', '') - deleted_images = data.get('deleted_images', []) - new_images = data.get('new_images', []) - - # ---------- 参数校验 ---------- - if not dingdan_id: - return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - if not isinstance(deleted_images, list): - return Response({'code': 2, 'msg': 'deleted_images 必须为数组', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - if not isinstance(new_images, list): - return Response({'code': 3, 'msg': 'new_images 必须为数组', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - if liuyan and len(liuyan) > 50: - return Response({'code': 4, 'msg': '留言最多50个字', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # ---------- 用户身份验证 ---------- - current_user = request.user - yonghuid = current_user.yonghuid - - # 检查是否为打手 - try: - dashou_profile = current_user.dashou_profile # 假设User模型有related_name='dashou_profile' - except UserDashou.DoesNotExist: - return Response({'code': 5, 'msg': '用户不是打手身份', 'data': None}, - status=status.HTTP_403_FORBIDDEN) - - # ---------- 订单查询及权限验证 ---------- - try: - # 订单必须属于该打手,且状态为4(退款中)或8(结算中) - dingdan = Dingdan.objects.get( - dingdan_id=dingdan_id, - jiedan_dashou_id=yonghuid, - zhuangtai__in=[4, 8] # 只允许修改退款中或结算中的订单 - ) - except Dingdan.DoesNotExist: - return Response({'code': 6, 'msg': '订单不存在或没有修改权限', 'data': None}, - status=status.HTTP_403_FORBIDDEN) - - # ---------- 执行修改(使用事务保证原子性) ---------- - try: - with transaction.atomic(): - # 1. 处理删除的图片:从 Dashoutupian 表中删除记录 - if deleted_images: - # 根据订单ID、打手ID和图片URL删除记录 - delete_count, _ = Dashoutupian.objects.filter( - dingdan_id=dingdan_id, - dashou=yonghuid, - tupian__in=deleted_images - ).delete() - # 可选:记录删除数量 - logger.info(f"订单 {dingdan_id} 删除图片 {delete_count} 张") - - # 2. 处理新增的图片:插入新记录 - if new_images: - # 批量创建 - new_records = [] - for url in new_images: - new_records.append(Dashoutupian( - dingdan_id=dingdan_id, - dashou=yonghuid, - tupian=url, - create_time=timezone.now() - )) - Dashoutupian.objects.bulk_create(new_records) - logger.info(f"订单 {dingdan_id} 新增图片 {len(new_records)} 张") - - # 3. 更新订单留言(如果发生变化) - # 注意:即使留言为空字符串,也允许更新,表示清空留言 - if 'liuyan' in data: # 判断是否传了liuyan字段(可能为空字符串) - dingdan.dashou_liuyan = liuyan - dingdan.update_time = timezone.now() - dingdan.save(update_fields=['dashou_liuyan', 'update_time']) - - # 全部成功,返回成功响应 - return Response({ - 'code': 0, - 'msg': '修改成功', - 'data': None - }, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f"修改订单失败: {str(e)}", exc_info=True) - return Response({ - 'code': 7, - 'msg': '服务器内部错误', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - - - - - -from django.core.exceptions import ObjectDoesNotExist -from dingdan.tongzhi_tasks import dingdan_guangbo - -class ZxsjghdsView(APIView): - """ - 商家更换打手接口 - 路径:POST /dingdan/zxsjghds - 权限:仅限已认证商家(通过JWT) - 功能: - 1. 将原订单状态改为已退款(5),释放接单打手(若有) - 2. 更新打手统计(退款量、状态、每日统计) - 3. 写入退款记录 - 4. 若为跨平台订单,通知对方平台退款(必须成功) - 5. 基于原订单生成新订单(状态直接为1),复制核心数据 - 6. 不操作商家余额、退款次数等(替换订单) - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # ========== 1. 获取当前商家用户 ========== - current_user = request.user - try: - yonghuid = current_user.yonghuid - except AttributeError: - return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # ========== 2. 提取订单ID ========== - dingdan_id = request.data.get('dingdan_id', '').strip() - if not dingdan_id: - return Response({'code': 2, 'msg': '订单ID不能为空'}, status=status.HTTP_400_BAD_REQUEST) - - # ========== 3. 查询订单并校验权限 ========== - try: - # 关联查询商家扩展表,确保当前商家是该订单的发布者 - shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').get( - dingdan__dingdan_id=dingdan_id, - shangjia_id=yonghuid - ) - except DingdanShangjia.DoesNotExist: - return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN) - - old_order = shangjia_kuozhan.dingdan - - # ========== 4. 状态校验(仅允许进行中或结算中) ========== - if old_order.zhuangtai not in [2, 8]: - return Response({'code': 4, 'msg': '当前订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST) - - # ========== 5. 收集订单关键数据(用于新订单复制) ========== - # 这些字段将在退款处理后用于创建新订单 - 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 - fadan_pingtai = old_order.fadan_pingtai - - # 跨平台相关 - 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 - - dashou_id = old_order.jiedan_dashou_id # 可能为空 - - # ========== 6. 退款处理(事务块1:退款原订单 + 打手统计 + 跨平台通知) ========== - try: - with transaction.atomic(): - # 6.1 订单状态更新为已退款 - old_order.zhuangtai = 5 - old_order.save(update_fields=['zhuangtai']) - - # 6.2 释放打手 - if dashou_id: - try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1 - dashou_profile.zhuangtai = 1 # 空闲 - dashou_profile.save(update_fields=['tuikuanliang', 'zhuangtai']) - except (UserMain.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"打手 {dashou_id} 不存在或扩展表缺失,跳过更新") - - # 6.3 更新退款记录 - Tuikuanjilu.objects.update_or_create( - dingdan_id=dingdan_id, - defaults={ - 'dashouid': dashou_id or '', - 'qingqiuid': yonghuid, # 申请人为当前商家 - 'chuliid': yonghuid, # 处理人也是商家自己 - 'liyou': '商家更换打手', - 'sqzhuangtai': 1, # 状态 1=成功 - 'jine': jine or 0, - 'dashou_fencheng': dashou_fencheng or 0, - 'jieshao': jieshao or '', - 'beizhu': '更换打手系统自动退款', - 'nicheng': nicheng or '', - } - ) - - # 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, - action=3 # 3=退款 - ) - except Exception as e: - logger.error(f"更新打手每日统计失败: {e}") - # 统计失败不应阻断主流程,但可记录 - - # 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("我方俱乐部配置不存在") - 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.rstrip('/') - - notify_url = f"{partner_domain}/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: - transaction.set_rollback(True) - logger.error(f"对方平台接口HTTP错误: {resp.status_code}") - return Response({'code': 500, 'msg': '通知对方平台失败'}) - 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')}"}) - - # 更新跨平台扩展数据 - 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)}'}) - - except Exception as e: - logger.error(f"退款处理事务失败: {e}", exc_info=True) - return Response({'code': 500, 'msg': '退款处理失败'}) - - # ========== 7. 生成新订单(事务块2) ========== - try: - with transaction.atomic(): - # 7.1 生成新的订单ID - 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}" - - new_dingdan_id = generate_dingdan_id() - # 确保唯一 - while Dingdan.objects.filter(dingdan_id=new_dingdan_id).exists(): - new_dingdan_id = generate_dingdan_id() - - # 7.2 创建新订单(复制数据,状态为 1) - new_order = Dingdan.objects.create( - dingdan_id=new_dingdan_id, - zhuangtai=1, # 已下单 - fadan_pingtai=fadan_pingtai, - jine=jine, - dashou_fencheng=dashou_fencheng, - leixing_id=leixing_id, - yaoqiuleixing=yaoqiuleixing, - huiyuan_id=huiyuan_id, - yongjin=yongjin, - jieshao=jieshao, - nicheng=nicheng, - beizhu=beizhu, - user1_id=user1_id, # 商家标识 - tupian=tupian, - # 不继承:zhiding_id, jiedan_dashou_id, dashou_liuyan 等 - ) - - # 7.3 创建商家扩展表(关联新订单) - DingdanShangjia.objects.create( - dingdan=new_order, - shangjia_id=yonghuid, - sjnicheng=shangjia_kuozhan.sjnicheng or '', - ) - - # 7.4 异步通知打手(广播新订单) - try: - order_info = { - 'dingdan_id': new_dingdan_id, - 'game_type': self._get_game_type_name(leixing_id), - 'amount': str(jine), - 'order_desc': (jieshao or '')[:50], - } - dingdan_guangbo.delay(order_info) - except Exception as e: - logger.error(f"广播通知失败: {e}") - - # 7.5 跨平台同步(如果适用) - try: - sync_order_to_partners(new_dingdan_id) - except Exception as e: - logger.error(f"跨平台同步失败: {e}") - - # 注意:此处不更新商家余额、发布量等统计,因为是替换订单 - - logger.info(f"商家{yonghuid}更换打手成功,旧订单{dingdan_id}已退款,新订单{new_dingdan_id}") - - return Response({ - 'code': 0, - 'msg': '更换成功', - 'data': { - 'xin_dingdan_id': new_dingdan_id, - } - }) - - except Exception as e: - logger.error(f"生成新订单事务失败: {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 '游戏订单' - - - -# ===================== 视图:商家罚款申请 ===================== -# 文件路径:dingdan/views.py 或单独文件,根据项目结构放置 - - - - -class ShangjiaFakuanApplyView(APIView): - """ - 商家罚款申请接口 - 路径:POST /dingdan/sjfksq - 权限:JWT认证的商家用户 - 参数: - dingdan_id - 订单ID (必填) - chufa_liyou - 罚款原因 (必填) - fakuanjine - 罚款金额,元 (必填) - yingxiang_qiangdan - 是否影响抢单 1是 0否 (必填) - 说明: - 1. 自动从订单中获取被打手ID,防止商家篡改目标。 - 2. 同一订单同一打手不允许重复罚款。 - 3. 跨平台订单若为我方派单,则通知对方平台处罚;否则本地生成罚单。 - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # ========== 1. 身份验证 ========== - current_user = request.user - try: - shangjia_id = current_user.yonghuid # 商家的用户ID - except AttributeError: - return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # ========== 2. 参数提取与校验 ========== - dingdan_id = request.data.get('dingdan_id', '').strip() - chufa_liyou = request.data.get('chufa_liyou', '').strip() - fakuanjine = request.data.get('fakuanjine', 0) - yingxiang_qiangdan = int(request.data.get('yingxiang_qiangdan', 1)) - - if not dingdan_id or not chufa_liyou or not fakuanjine: - return Response({'code': 2, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) - - try: - fakuanjine = float(fakuanjine) - if fakuanjine <= 0: - raise ValueError - except (ValueError, TypeError): - return Response({'code': 2, 'msg': '罚款金额格式错误'}, status=status.HTTP_400_BAD_REQUEST) - - # ========== 3. 获取订单,校验归属商家 ========== - try: - # 关联查询商家扩展表,确保订单属于当前商家 - shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').get( - dingdan__dingdan_id=dingdan_id, - shangjia_id=shangjia_id - ) - order = shangjia_kuozhan.dingdan - except DingdanShangjia.DoesNotExist: - return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN) - - # ========== 4. 获取被打手ID ========== - dashou_id = order.jiedan_dashou_id - if not dashou_id: - return Response({'code': 4, 'msg': '该订单暂无接单打手,无法罚款'}, status=status.HTTP_400_BAD_REQUEST) - - # ========== 5. 重复罚款检查 ========== - if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): - return Response({'code': 5, 'msg': '该打手在此订单已被罚款,不能重复申请'}, status=status.HTTP_400_BAD_REQUEST) - - # ========== 6. 跨平台信息 ========== - is_cross = order.is_cross - partner_order_id = order.partner_order_id or '' - partner_club_id = order.partner_club_id or '' - dispatch_type = order.dispatch_type or 0 # 1=我方派单,2=对方派单 - - # 是否需要通知对方平台:我方派单且存在对方订单ID - need_notify_partner = is_cross and partner_order_id and dispatch_type == 1 - - # ========== 7. 本地创建罚单(可重用) ========== - def create_local_fadan(): - Fadan.objects.create( - beichufa_id=dashou_id, - shenqing_chufa=shangjia_id, # 申请人为当前商家 - shenfen=1, # 被打者身份:1=打手 - chufaliyou=chufa_liyou, - fakuanjine=fakuanjine, - guanliandingdan_id=dingdan_id, - zhuangtai=1, # 待缴纳 - yingxiang_qiangdan=yingxiang_qiangdan, - shenqingren_shenfen=5, # 申请人是商家 - partner_club_id='', - ) - - # ========== 8. 本地处罚分支 ========== - if not need_notify_partner: - with transaction.atomic(): - create_local_fadan() - logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}罚款成功,金额{fakuanjine}") - return Response({'code': 0, 'msg': '罚款申请已提交'}) - - # ========== 9. 跨平台通知分支 ========== - try: - # 9.1 获取我方俱乐部ID - my_club = ClubConfig.objects.filter(is_self=1).first() - if not my_club: - return Response({'code': 500, 'msg': '系统配置错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - self_club_id = my_club.club_id - - # 9.2 获取对方平台域名 - club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) - partner_domain = club_rel.partner_domain.rstrip('/') - except ObjectDoesNotExist: - return Response({'code': 500, 'msg': '对方平台信息未配置'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 9.3 构造通知参数 - notify_url = f"{partner_domain}/houtai/kffkdssq_notify" # 与后台接口保持一致 - payload = { - 'order_id': partner_order_id, - 'club_id': self_club_id, - 'chufaliyou': chufa_liyou, - 'fakuanjine': fakuanjine, - 'yingxiang_qiangdan': yingxiang_qiangdan, - 'shenqingren_shenfen': 5, # 商家身份 - 'operator': shangjia_id, # 操作人ID - } - - # 9.4 带重试的通知 - timeout = 3 - retries = 2 - success = False - for attempt in range(retries): - try: - resp = requests.post(notify_url, json=payload, timeout=timeout) - if resp.status_code == 200: - resp_data = resp.json() - if resp_data.get('code') == 0: - success = True - break - else: - logger.warning(f"对方平台罚款通知失败,响应: {resp_data}") - else: - logger.warning(f"对方平台HTTP错误: {resp.status_code}") - except Exception as e: - logger.warning(f"通知对方平台异常,第{attempt+1}次尝试: {e}") - continue - - if not success: - return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 9.5 通知成功后,本地也创建罚单(标记跨平台) - with transaction.atomic(): - Fadan.objects.create( - beichufa_id=dashou_id, - shenqing_chufa=shangjia_id, - shenfen=1, - chufaliyou=chufa_liyou, - fakuanjine=fakuanjine, - guanliandingdan_id=dingdan_id, - zhuangtai=1, - yingxiang_qiangdan=yingxiang_qiangdan, - shenqingren_shenfen=5, - partner_club_id=partner_club_id, # 记录对方俱乐部ID - ) - - logger.info(f"商家{shangjia_id}跨平台罚款成功: {dingdan_id}") - return Response({'code': 0, 'msg': '罚款已提交至对方平台'}) - - - - - - - - - - -class ShangjiaFakuaiXiugaiView(APIView): - """ - 商家端统一修改接口:罚款修改/撤销/再次申请,订单退款撤销/修改理由 - URL: /dingdan/sjfkxgycx - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - user = request.user - # 验证商家身份 - try: - shangjia = user.shop_profile - except UserShangjia.DoesNotExist: - return Response({'code': 403, 'msg': '您不是商家'}, status=status.HTTP_403_FORBIDDEN) - if shangjia.zhuangtai != 1: - return Response({'code': 403, 'msg': '商家状态异常'}, status=status.HTTP_403_FORBIDDEN) - - operation = request.data.get('operation') - dingdan_id = request.data.get('dingdan_id') - if not dingdan_id: - return Response({'code': 400, 'msg': '缺少订单ID'}, status=status.HTTP_400_BAD_REQUEST) - - # 验证订单属于该商家 - try: - dingdan = 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 not hasattr(dingdan, 'shangjia_kuozhan') or dingdan.shangjia_kuozhan.shangjia_id != user.yonghuid: - return Response({'code': 403, 'msg': '无权操作此订单'}, status=status.HTTP_403_FORBIDDEN) - - # 处理订单相关操作 - if operation == 'cancel_refund': - return self._cancel_refund(dingdan, request.data.get('liyou', '')) - elif operation == 'modify_refund_reason': - return self._modify_refund_reason(dingdan, request.data.get('liyou', '')) - - # 处理罚款相关操作 - 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) - except Fadan.DoesNotExist: - return Response({'code': 404, 'msg': '罚单不存在'}, status=status.HTTP_404_NOT_FOUND) - if fadan.guanliandingdan_id != dingdan_id: - return Response({'code': 403, 'msg': '罚单与订单不匹配'}, status=status.HTTP_403_FORBIDDEN) - - if operation == 'modify_penalty': - return self._modify_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine')) - elif operation == 'cancel_penalty': - return self._cancel_penalty(fadan, request.data.get('liyou', '')) - elif operation == 'resubmit_penalty': - return self._resubmit_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine')) - else: - return Response({'code': 400, 'msg': '无效的操作类型'}, status=status.HTTP_400_BAD_REQUEST) - - def _cancel_refund(self, dingdan, liyou): - """撤销退款申请(状态4 → 状态8,清空退款理由)""" - if dingdan.zhuangtai != 4: - return Response({'code': 400, 'msg': '只有退款中的订单可以撤销退款申请'}) - with transaction.atomic(): - dingdan.zhuangtai = 8 - dingdan.tkly = '' # 清空退款理由 - dingdan.save() - # 记录撤销理由可存入日志(如需要可另建日志表,此处简单处理) - logger.info(f"商家撤销退款订单 {dingdan.dingdan_id},理由: {liyou}") - return Response({'code': 0, 'msg': '已撤销退款申请'}) - - def _modify_refund_reason(self, dingdan, liyou): - """修改退款理由(仅状态4时)""" - if dingdan.zhuangtai != 4: - return Response({'code': 400, 'msg': '只有退款中的订单可以修改退款理由'}) - if not liyou.strip(): - return Response({'code': 400, 'msg': '退款理由不能为空'}) - dingdan.tkly = liyou[:500] - dingdan.save() - return Response({'code': 0, 'msg': '退款理由修改成功'}) - - def _modify_penalty(self, fadan, liyou, jine_str): - """修改罚款(仅状态1待缴纳 或 3申诉中)""" - if fadan.zhuangtai not in (1, 3): - return Response({'code': 400, 'msg': '当前罚单状态不可修改'}) - if not liyou.strip(): - return Response({'code': 400, 'msg': '罚款理由不能为空'}) - try: - jine = Decimal(jine_str) - if jine <= 0: - raise ValueError - except: - return Response({'code': 400, 'msg': '罚款金额必须为正数'}) - with transaction.atomic(): - fadan.chufaliyou = liyou[:500] - fadan.fakuanjine = jine - # 如果之前是驳回状态修改,需要重置状态为待缴纳?不,修改不应改变状态 - fadan.save() - return Response({'code': 0, 'msg': '罚款修改成功'}) - - def _cancel_penalty(self, fadan, liyou): - """撤销罚款(状态1或3 → 状态4 已驳回)""" - if fadan.zhuangtai not in (1, 3): - return Response({'code': 400, 'msg': '当前罚单状态不可撤销'}) - if not liyou.strip(): - return Response({'code': 400, 'msg': '驳回理由不能为空'}) - with transaction.atomic(): - fadan.zhuangtai = 4 # 已驳回 - fadan.bohuiliyou = liyou[:500] - fadan.save() - return Response({'code': 0, 'msg': '罚款已撤销(驳回)'}) - - def _resubmit_penalty(self, fadan, liyou, jine_str): - """再次申请罚款(状态4 → 状态1,修改理由和金额)""" - if fadan.zhuangtai != 4: - return Response({'code': 400, 'msg': '只有已驳回的罚单可再次申请'}) - if not liyou.strip(): - return Response({'code': 400, 'msg': '罚款理由不能为空'}) - try: - jine = Decimal(jine_str) - if jine <= 0: - raise ValueError - except: - return Response({'code': 400, 'msg': '罚款金额必须为正数'}) - with transaction.atomic(): - fadan.zhuangtai = 1 # 待缴纳 - fadan.chufaliyou = liyou[:500] - fadan.fakuanjine = jine - fadan.bohuiliyou = '' # 清空驳回理由 - fadan.save() +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 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, DingdanShangjia, Chufajilu, Liaotian, Chufatupian +from utils.weixin_broadcast import WeixinBroadcastSender +from yonghu.models import UserMain, UserDashou, UserShangjia + +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 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 + +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): + """ + 前端支付失败接口 - 处理用户主动取消或支付失败 + 只允许删除状态为9(未支付)的订单 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}) + + with transaction.atomic(): + # 锁定订单防止并发 + dingdan = Dingdan.objects.select_for_update().filter(dingdan_id=dingdanid).first() + if not dingdan: + return Response({'code': 2, 'msg': '订单不存在', 'data': None}) + + if dingdan.user1_id != f"Boss{request.user.yonghuid}": + return Response({'code': 3, 'msg': '无权操作此订单', 'data': None}) + + # 只允许删除未支付订单(状态9) + if dingdan.zhuangtai != 9: + return Response({'code': 3, 'msg': '只能删除未支付的订单', 'data': None}) + + # 删除扩展表和主表 + DingdanPingtai.objects.filter(dingdan=dingdan).delete() + dingdan.delete() + + return Response({'code': 0, 'msg': '订单已删除', 'data': None}) + + except Exception as e: + 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): + """微信支付回调接口 - 接收微信官方支付结果通知(含详细调试日志)""" + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + + def post(self, request): + # 记录原始请求数据(调试用) + xml_data = request.body.decode('utf-8') + + # 最外层异常捕获,确保任何错误都能被记录并返回适当响应 + try: + # 1. 解析 XML + try: + root = ET.fromstring(xml_data) + except ET.ParseError as e: + logger.error(f"XML解析失败: {e}") + return self._gen_response_xml('FAIL', 'XML解析失败') + + # 2. 获取关键参数 + return_code = root.find('return_code').text if root.find('return_code') is not None else '' + result_code = root.find('result_code').text if root.find('result_code') is not None else '' + out_trade_no = root.find('out_trade_no').text if root.find('out_trade_no') is not None else '' + transaction_id = root.find('transaction_id').text if root.find('transaction_id') is not None else '' + + # 3. 签名验证 + if not self._verify_signature(xml_data): + logger.warning("签名验证失败") + return self._gen_response_xml('FAIL', '签名验证失败') + + # 4. 根据支付结果处理 + if return_code == 'SUCCESS' and result_code == 'SUCCESS': + self._handle_success(out_trade_no, transaction_id, root) + else: + logger.info(f"支付失败或异常,return_code={return_code}, result_code={result_code}") + self._handle_failure(out_trade_no) + + # 返回微信要求的成功响应(必须返回SUCCESS,即使内部处理失败也要返回,否则微信会重复通知) + return self._gen_response_xml('SUCCESS', 'OK') + + except Exception as e: + # 捕获任何未处理的异常,记录完整堆栈 + logger.error(f"回调处理发生未捕获异常: {str(e)}") + logger.error(traceback.format_exc()) + return self._gen_response_xml('FAIL', f'服务器内部错误: {str(e)}') + + def _verify_signature(self, xml_data): + """微信支付签名验证(MD5)""" + try: + root = ET.fromstring(xml_data) + sign = root.find('sign').text + # 构建待签名字符串(排除sign字段) + data = {child.tag: child.text for child in root if child.tag != 'sign'} + # 按key排序 + sorted_keys = sorted(data.keys()) + stringA = '&'.join([f"{k}={data[k]}" for k in sorted_keys]) + stringSignTemp = f"{stringA}&key={settings.WEIXIN_SHANGHUMIYAO}" + calc_sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() + logger.debug(f"计算签名: {calc_sign}, 微信签名: {sign}") + return calc_sign == sign + except Exception as e: + logger.error(f"签名验证过程异常: {e}") + logger.error(traceback.format_exc()) + return False + + def _gen_response_xml(self, return_code, return_msg): + """生成返回给微信的XML响应""" + xml = f""" + + +""" + logger.info(f"返回给微信的响应: {xml}") + return HttpResponse(xml, content_type='application/xml') + + @transaction.atomic + def _handle_success(self, out_trade_no, transaction_id, root): + """处理支付成功""" + logger.info(f"进入 _handle_success, out_trade_no={out_trade_no}, transaction_id={transaction_id}") + + # 锁定订单防止并发 + try: + dingdan = Dingdan.objects.select_for_update().filter(dingdan_id=out_trade_no).first() + except Exception as e: + logger.error(f"查询订单异常: {e}") + raise + + if not dingdan: + logger.error(f"订单不存在: {out_trade_no}") + return + + logger.info(f"订单当前状态: {dingdan.zhuangtai}") + + # 幂等处理:只有状态为9(未支付)才处理,已处理则跳过 + if dingdan.zhuangtai != 9: + logger.info(f"订单 {out_trade_no} 已处理过,状态={dingdan.zhuangtai},跳过") + return + + # 保存微信交易号 + if transaction_id: + dingdan.wechat_transaction_id = transaction_id + logger.info(f"保存微信交易号: {transaction_id}") + else: + logger.warning("微信未返回transaction_id") + + # 更新订单状态 + if dingdan.zhiding_id and dingdan.zhiding_id.strip(): + dingdan.zhuangtai = 7 + logger.info("指定单,状态设为7") + else: + dingdan.zhuangtai = 1 + logger.info("普通单,状态设为1") + + dingdan.save() + + logger.info(f"订单状态已更新为 {dingdan.zhuangtai}") + + # 删除同一用户同一商品的其他未支付订单(改为逻辑删除,状态25) + try: + abandoned_count = Dingdan.objects.filter( + user1_id=dingdan.user1_id, + shangpin_id=dingdan.shangpin_id, + zhuangtai=9 + ).exclude(dingdan_id=out_trade_no).update(zhuangtai=25) + logger.info(f"将 {abandoned_count} 个未支付订单标记为已废弃(25)") + except Exception as e: + logger.error(f"标记其他未支付订单异常: {e}") + + # 更新老板扩展表 + try: + laoban_id = None + if hasattr(dingdan, 'pingtai_kuozhan'): + laoban_id = dingdan.pingtai_kuozhan.laoban_id + if laoban_id: + user_main = UserMain.objects.filter(yonghuid=laoban_id).first() + if user_main and hasattr(user_main, 'boss_profile'): + boss = user_main.boss_profile + boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0) + boss.alldingdan = (boss.alldingdan or 0) + 1 + boss.save() + logger.info(f"更新老板 {laoban_id} 数据成功") + except Exception as e: + logger.error(f"更新老板数据异常: {e}") + + # 更新商品库存 + try: + if dingdan.shangpin_id: + Shangpin.objects.filter(id=dingdan.shangpin_id).update( + kucun=F('kucun') - 1 + ) + logger.info(f"商品 {dingdan.shangpin_id} 库存减1") + except Exception as e: + logger.error(f"更新商品库存异常: {e}") + + + # ========== 新增:商品和店铺每日统计 ========== + try: + # 商品每日统计(只要有商品ID就执行) + if dingdan.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(dingdan, 'pingtai_kuozhan'): + pingtai_shouyi = dingdan.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=dingdan.shangpin_id, + caozuo_leixing=1, # 1 = 下单 + dingdan_jiage=dingdan.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {dingdan.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + dianpu_id = None + if hasattr(dingdan, 'pingtai_kuozhan'): + dianpu_id = dingdan.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=1, # 1 = 下单 + dingdan_jiage=dingdan.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + + # 更新收支记录(总累计) + self._update_szjilu(dingdan.jine) + + # 更新每日收入统计 + update_daily_income(dingdan.jine) + + # 跨平台同步 + 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, + '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) # 瞬间返回,不阻塞请求 + + # 异步广播通知 + #self._broadcast_async(dingdan) + + logger.info(f"订单 {out_trade_no} 支付成功处理完成") + + @transaction.atomic + def _handle_failure(self, out_trade_no): + """处理支付失败""" + logger.info(f"进入 _handle_failure, out_trade_no={out_trade_no}") + + try: + dingdan = Dingdan.objects.select_for_update().filter(dingdan_id=out_trade_no).first() + if not dingdan: + logger.warning(f"订单不存在: {out_trade_no}") + return + + logger.info(f"订单当前状态: {dingdan.zhuangtai}") + + # 仅当订单状态为9(未支付)时才标记失败,不直接删除 + if dingdan.zhuangtai == 9: + dingdan.zhuangtai = 14 # 自定义支付失败状态 + dingdan.save() + logger.info(f"订单 {out_trade_no} 支付失败,标记为14") + else: + logger.info(f"订单 {out_trade_no} 状态非9,不处理") + except Exception as e: + logger.error(f"处理支付失败异常: {e}") + logger.error(traceback.format_exc()) + + def _update_szjilu(self, jine): + """更新收支记录表(ID=1的记录)""" + try: + jine_decimal = Decimal(str(jine)) + with transaction.atomic(): + szjilu, created = Szjilu.objects.select_for_update().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') + } + ) + if created: + logger.info("创建ID=1的收支记录") + szjilu.zongsy += jine_decimal + szjilu.zongls += jine_decimal + szjilu.jrls += jine_decimal + szjilu.save() + logger.info(f"收支记录更新成功,增加金额: {jine}") + except Exception as e: + logger.error(f"更新收支记录失败: {e}") + + def _broadcast_async(self, dingdan): + """异步广播通知""" + 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), + 'amount': str(dingdan.jine), + 'order_desc': (dingdan.jieshao or '')[:50], + } + logger.info(f"准备广播订单: {order_info}") + sender = WeixinBroadcastSender() + result = sender.broadcast_order(order_info) + if result.get('success'): + logger.info(f"订单广播完成: {result.get('msg')}") + else: + logger.warning(f"订单广播异常: {result.get('msg')}") + except Exception as e: + logger.error(f"广播任务异常: {e}") + logger.error(traceback.format_exc()) + + thread = threading.Thread(target=task) + thread.daemon = True + thread.start() + logger.info("广播线程已启动") + except Exception as e: + logger.error(f"启动广播线程异常: {e}") + + 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 '游戏订单' + + +from django.core.cache import cache +class PaymentVerifyView(APIView): + """ + 订单支付成功复查接口 - 前端支付成功后调用,确认订单状态 + 当订单状态为9时,主动查询微信支付状态,若已支付则更新本地状态 + 需要token认证 + """ + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + # ---------- 1. 获取参数 ---------- + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 1, 'msg': '订单ID不能为空'}) + + # ---------- 2. 查询订单 ---------- + dingdan = Dingdan.objects.filter(dingdan_id=dingdanid).first() + if not dingdan: + return Response({'code': 2, 'msg': '订单不存在'}) + + # ---------- 3. 验证订单归属(安全) ---------- + current_user = request.user + user_identifier = f"Boss{current_user.yonghuid}" + if dingdan.user1_id != user_identifier: + logger.warning(f"用户 {current_user.yonghuid} 尝试复查他人订单 {dingdanid}") + return Response({'code': 403, 'msg': '无权访问'}) + + # ---------- 4. 如果订单状态已是1/7,直接返回成功 ---------- + if dingdan.zhuangtai in [1, 7]: + return Response({ + 'code': 0, + 'msg': '订单状态正常', + 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai} + }) + + # ---------- 5. 如果状态为9,则主动查询微信支付状态 ---------- + if dingdan.zhuangtai == 9: + # 使用分布式锁防止并发处理同一订单 + lock_key = f"payment_verify_lock_{dingdanid}" + if not cache.add(lock_key, "locked", timeout=10): + # 已有其他进程在处理,让前端稍后重试 + return Response({'code': 1, 'msg': '订单正在处理中,请稍后'}) + + try: + # 调用微信订单查询接口 + trade_state, transaction_id = self._query_wechat_payment(dingdanid) + + if trade_state == 'SUCCESS': + # 支付成功,执行本地更新 + self._process_payment_success(dingdan, transaction_id, current_user.yonghuid) + # 重新获取订单以获取最新状态 + dingdan.refresh_from_db() + return Response({ + 'code': 0, + 'msg': '支付成功', + 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai} + }) + elif trade_state in ['NOTPAY', 'USERPAYING']: + # 用户尚未支付或支付中 + return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}}) + else: + # 其他状态:CLOSED, REVOKED, PAYERROR 等,视为支付失败 + return Response({'code': 2, 'msg': '支付失败'}) + + except Exception as e: + logger.error(f"查询微信支付异常: {e}", exc_info=True) + return Response({'code': 1, 'msg': '查询失败,请稍后重试'}) + finally: + cache.delete(lock_key) + else: + # ---------- 6. 其他异常状态 ---------- + status_msg = { + 2: '订单已取消', + 3: '订单已完成', + 4: '退款审核中', + 5: '已退款', + 6: '退款失败', + 8: '结算中', + 0: '订单已作废', + }.get(dingdan.zhuangtai, '订单状态异常') + return Response({'code': 3, 'msg': status_msg, 'data': {'zhuangtai': dingdan.zhuangtai}}) + + except Exception as e: + logger.error(f"订单复查接口异常: {e}", exc_info=True) + return Response({'code': 99, 'msg': f'系统错误: {str(e)}'}) + + def _process_payment_success(self, dingdan, transaction_id, operator_id): + """ + 支付成功后的原子操作(与微信回调逻辑完全一致) + 在事务内完成所有状态更新和数据一致性操作 + """ + with transaction.atomic(): + # 重新锁定订单,防止并发更新 + dingdan = Dingdan.objects.select_for_update().get(pk=dingdan.pk) + if dingdan.zhuangtai != 9: + # 订单状态已被其他进程更新,直接返回(幂等) + return + + # 更新订单状态 + dingdan.zhuangtai = 7 if dingdan.zhiding_id and dingdan.zhiding_id.strip() else 1 + dingdan.wechat_transaction_id = transaction_id + dingdan.clkf = operator_id # 可选:记录操作人ID + dingdan.save() + + # 删除同一用户同一商品的其他未支付订单(逻辑删除,状态25) + Dingdan.objects.filter( + user1_id=dingdan.user1_id, + shangpin_id=dingdan.shangpin_id, + zhuangtai=9 + ).exclude(dingdan_id=dingdan.dingdan_id).update(zhuangtai=25) + + # 更新老板扩展表 + laoban_id = dingdan.pingtai_kuozhan.laoban_id if hasattr(dingdan, 'pingtai_kuozhan') else None + if laoban_id: + user_main = UserMain.objects.filter(yonghuid=laoban_id).first() + if user_main and hasattr(user_main, 'boss_profile'): + boss = user_main.boss_profile + boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0) + boss.alldingdan = (boss.alldingdan or 0) + 1 + boss.save() + + # 更新商品库存 + if dingdan.shangpin_id: + Shangpin.objects.filter(id=dingdan.shangpin_id).update( + kucun=F('kucun') - 1 + ) + + + # ========== 新增:商品和店铺每日统计 ========== + try: + # 商品每日统计(只要有商品ID就执行) + if dingdan.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(dingdan, 'pingtai_kuozhan'): + pingtai_shouyi = dingdan.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=dingdan.shangpin_id, + caozuo_leixing=1, # 1 = 下单 + dingdan_jiage=dingdan.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {dingdan.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + dianpu_id = None + if hasattr(dingdan, 'pingtai_kuozhan'): + dianpu_id = dingdan.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=1, # 1 = 下单 + dingdan_jiage=dingdan.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + + # 更新收支记录 + self._update_szjilu(dingdan.jine) + + # 更新每日收入统计 + update_daily_income(dingdan.jine) + + # 跨平台同步(复查成功也需要通知) + sync_order_to_partners(dingdan.dingdan_id) + + def _query_wechat_payment(self, out_trade_no): + """ + 调用微信支付订单查询接口,返回 (trade_state, transaction_id) + 若查询失败,抛出异常 + """ + appid = settings.WEIXIN_APPID + mch_id = settings.WEIXIN_MCHID + key = settings.WEIXIN_SHANGHUMIYAO + nonce_str = self._generate_nonce_str() + + params = { + 'appid': appid, + 'mch_id': mch_id, + 'out_trade_no': out_trade_no, + 'nonce_str': nonce_str, + } + + # 生成签名 + 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/pay/orderquery' + response = requests.post(url, data=xml_data, timeout=10) + result = xmltodict.parse(response.content)['xml'] + + if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': + return result.get('trade_state'), result.get('transaction_id') + else: + err_msg = result.get('return_msg', '未知错误') + raise Exception(f"微信订单查询失败: {err_msg}") + + def _update_szjilu(self, jine): + """更新收支记录表(ID=1的记录)""" + try: + jine_decimal = Decimal(str(jine)) + with transaction.atomic(): + szjilu, created = Szjilu.objects.select_for_update().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') + } + ) + if created: + logger.info("创建ID=1的收支记录") + szjilu.zongsy += jine_decimal + szjilu.zongls += jine_decimal + szjilu.jrls += jine_decimal + szjilu.save() + logger.info(f"收支记录更新成功,增加金额: {jine}") + except Exception as e: + logger.error(f"更新收支记录失败: {e}") + # 不抛出异常,因为收支记录不是核心流程,但记录错误供排查 + + def _generate_nonce_str(self): + """生成随机字符串,长度32""" + return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + def _dict_to_xml(self, data): + """将字典转换为XML字符串""" + xml = [''] + for k, v in data.items(): + xml.append(f'<{k}>{v}') + xml.append('') + 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): + """ + 创建订单并调用微信支付接口 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + # 1. 获取前端传递的字段 + shangpin_id = request.data.get('shangpin_id') + nicheng = request.data.get('nicheng', '').strip() + beizhu = request.data.get('beizhu', '').strip() + zhiding = request.data.get('zhiding', '').strip() + jine = request.data.get('jine', 0) + + # 2. 验证必填字段 + if not shangpin_id: + return Response({'code': 1, 'msg': '商品ID不能为空', 'data': None}) + + if not nicheng: + return Response({'code': 2, 'msg': '游戏昵称不能为空', 'data': None}) + + if len(nicheng) > 50: + return Response({'code': 3, 'msg': '游戏昵称过长', 'data': None}) + + # 3. 查询商品信息 + shangpin = Shangpin.objects.filter(id=shangpin_id).first() + if not shangpin: + return Response({'code': 4, 'msg': '商品不存在', 'data': None}) + + + is_valid, err_msg, _ = validate_shangpin_and_dianpu(shangpin_id) + if not is_valid: + return Response({'code': 5, 'msg': err_msg, 'data': None}) + + # 检查库存 + if hasattr(shangpin, 'kucun') and shangpin.kucun <= 0: + return Response({'code': 5, 'msg': '商品库存不足', 'data': None}) + + # 获取商品价格 + shangpin_jiage = getattr(shangpin, 'jiage', 0) + if not shangpin_jiage: + return Response({'code': 6, 'msg': '商品价格异常', 'data': None}) + #获取商品图片 + shangpin_tupian = getattr(shangpin, 'tupian_url', None) + + # 4. 验证指定打手(如果存在) + zhiding_id_value = None + if zhiding: + # 查询主表中是否存在该打手 + zhiding_user = UserMain.objects.filter( + yonghuid=zhiding, + #user_type='dashou' + ).first() + + if not zhiding_user: + return Response({'code': 7, 'msg': '指定打手不存在', 'data': None}) + + # 查询打手扩展表 + dashou_profile = UserDashou.objects.filter(user=zhiding_user).first() + + if not dashou_profile: + return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None}) + + # 检查在线状态 + if getattr(dashou_profile, 'zhanghaozhuangtai', 0) == 0: + return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None}) + + zhiding_id_value = zhiding + + # 5. 生成订单ID + timestamp = int(time.time()) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + dingdanid = f"PT{timestamp:011d}{timestamp_ns:06d}{random_num:02d}" + + # 6. 查询利率并计算分成 + # 查询打手利率(字符'1') + dashou_lilu = Lilubiao.objects.filter(fadanpingtai='1').first() + + + # 🔥 新增:判断是否使用商品配置的额外分成 + if shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None: + # 使用商品指定的额外分成金额 + dashou_fencheng = shangpin.ewai_dashou_fencheng + else: + # 按原有逻辑计算打手分成 + dashou_fencheng = Decimal(str(shangpin_jiage)) * Decimal(str(dashou_lilu.lilu)) + + + # 查询管事利率(字符'2') + #guanshi_lilu = Lilubiao.objects.filter(fadanpingtai='2').first() + + if not dashou_lilu: #or not guanshi_lilu: + return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None}) + + # 计算打手分成 + #dashou_fencheng = Decimal(str(shangpin_jiage)) * Decimal(str(dashou_lilu.lilu)) + + + # 开始数据库事务 + with transaction.atomic(): + # 获取当前用户信息 + current_user = request.user + laoban_yonghuid = getattr(current_user, 'yonghuid', '') + user_openid = getattr(current_user, 'openid', '') + + if not user_openid: + return Response({'code': 10, 'msg': '用户openid不存在', 'data': None}) + + # 创建订单主表数据 + order_data = { + 'dingdan_id': dingdanid, + 'zhuangtai': 9, # 未付款状态 + 'jine': shangpin_jiage, + 'fadan_pingtai':1, + 'dashou_fencheng': dashou_fencheng, + 'zhiding_id': zhiding_id_value, + 'shangpin_id': shangpin_id, + 'yaoqiuleixing': getattr(shangpin, 'yaoqiuleixing', None), + 'huiyuan_id': getattr(shangpin, 'huiyuan_id', ''), + 'yongjin': getattr(shangpin, 'yongjin', 0), + 'jieshao': getattr(shangpin, 'jieshao', ''), + 'beizhu': beizhu, + 'nicheng': nicheng, + 'tupian':shangpin_tupian, + 'user1_id':f"Boss{request.user.yonghuid}", + 'leixing_id':shangpin.leixing_id + } + + # 创建订单记录 + + dingdan = Dingdan.objects.create(**order_data) + + + # CreateOrderView.post 事务内部,替换原有的扩展表创建代码 + + dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong = calculate_pingtai_and_dianpu_shouyi( + shangpin_id=shangpin_id, + dingdan_jiage=shangpin_jiage, + dashou_fencheng=dashou_fencheng + ) + + pingtaikuozhan_data = { + 'dingdan': dingdan, + 'laoban_id': laoban_yonghuid, + 'pingtai_shouyi': pingtai_shouyi, + # ---------- 新增上级分红快照 ---------- + 'shangji_dianpu_id': shangji_dianpu_id, + 'shangji_fenhong': shangji_fenhong if shangji_fenhong else Decimal('0.00'), + } + if dianpu_id: + pingtaikuozhan_data['dianpu_id'] = dianpu_id + pingtaikuozhan_data['dianpu_shouyi'] = dianpu_shouyi + else: + pingtaikuozhan_data['dianpu_shouyi'] = Decimal('0.00') + + DingdanPingtai.objects.create(**pingtaikuozhan_data) + + + # 计算收益 + '''dianpu_id, pingtai_shouyi, dianpu_shouyi = calculate_pingtai_and_dianpu_shouyi( + shangpin_id=shangpin_id, + dingdan_jiage=shangpin_jiage, + dashou_fencheng=dashou_fencheng + ) + + # 构建扩展表数据,平台收益必定保存,店铺收益和店铺ID仅当dianpu_id存在时才赋值 + pingtaikuozhan_data = { + 'dingdan': dingdan, + 'laoban_id': laoban_yonghuid, + 'pingtai_shouyi': pingtai_shouyi, + } + if dianpu_id: + pingtaikuozhan_data['dianpu_id'] = dianpu_id + pingtaikuozhan_data['dianpu_shouyi'] = dianpu_shouyi + else: + # 无店铺时,店铺收益字段保持默认(0.00),也可显式设为0 + pingtaikuozhan_data['dianpu_shouyi'] = Decimal('0.00') + + DingdanPingtai.objects.create(**pingtaikuozhan_data)''' + + + # 7. 生成微信支付参数(向微信官方发送请求) + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=shangpin_jiage, + beizhu=getattr(settings, 'WEIXIN_PAY_DESCRIPTION'), + user_ip=self.get_client_ip(request), + openid=user_openid + ) + + if not pay_params: + # 手动抛出异常触发事务回滚 + raise Exception('支付参数生成失败') + + # 8. 返回成功响应 + response_data = { + 'code': 0, + 'msg': '订单创建成功', + 'data': { + 'dingdanid': dingdanid, + 'payParams': pay_params + } + } + + return Response(response_data) + + except Exception as e: + # 这里会自动回滚事务 + return Response({'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None}) + + 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] + else: + ip = request.META.get('REMOTE_ADDR') + return ip + + def generate_wechat_pay_params(self, dingdanid, jine, beizhu, user_ip, openid): + """ + 调用微信支付统一下单接口,生成支付参数 + 完全按照您提供的正确示例代码编写 + """ + try: + # 微信支付配置 + APPID = getattr(settings, 'WEIXIN_APPID', '') + MCHID = getattr(settings, 'WEIXIN_MCHID', '') + KEY = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') + NOTIFY_URL = getattr(settings, 'WEIXIN_NOTIFY_URL', '') + + if not all([APPID, MCHID, KEY, NOTIFY_URL]): + raise Exception('微信支付配置不完整') + + # 生成随机字符串 + import string + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + # 金额(单位:分) + total_fee = int(float(jine) * 100) + + # 交易类型 + trade_type = 'JSAPI' + + # 生成签名 + sign_data = { + 'appid': APPID, + 'mch_id': MCHID, + 'nonce_str': nonce_str, + 'body': beizhu, + 'out_trade_no': dingdanid, + 'total_fee': total_fee, + 'spbill_create_ip': user_ip, + 'notify_url': NOTIFY_URL, + 'trade_type': trade_type, + 'openid': openid + } + + # 按照参数名ASCII码从小到大排序 + 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}' + + # MD5签名 + sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + # 构造XML数据 + xml_data = f""" +{APPID} +{beizhu} +{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 + ) + + # 解析返回的XML + 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}' + + # 生成支付签名 + pay_sign_data = { + 'appId': APPID, + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': 'MD5' + } + + 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() + + # 返回支付参数(格式与前端完全匹配) + result = { + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': 'MD5', + 'paySign': pay_sign + } + + return result + else: + # 获取详细的错误信息 + err_code = root.find('err_code') + err_code_des = root.find('err_code_des') + return_msg = root.find('return_msg') + + 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}, " + if return_msg is not None: + error_details += f"return_msg: {return_msg.text}" + + raise Exception(f"微信支付下单失败: {error_details}") + + except Exception as e: + 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): + """订单获取接口""" + permission_classes = [IsAuthenticated] + + def post(self, request): + # 获取当前登录用户 + try: + current_user = request.user + yonghuid = current_user.yonghuid + except Exception as e: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }) + + # 获取前端参数 + try: + data = request.data + zhuangtai_list = data.get('zhuangtai_list', []) + page = int(data.get('page', 1)) + page_size = int(data.get('page_size', 10)) + except Exception as e: + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }) + + # 参数验证 + if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0: + return Response({ + 'code': 3, + 'msg': '状态列表不能为空', + 'data': None + }) + + if page < 1: + page = 1 + if page_size <= 0: + page_size = 10 + + try: + # 查询扩展表,获取用户的所有订单ID + pingtai_orders = DingdanPingtai.objects.filter(laoban_id=yonghuid) + pingtai_order_ids = list(pingtai_orders.values_list('dingdan__dingdan_id', flat=True)) + + if not pingtai_order_ids: + return Response({ + 'code': 0, + 'msg': '成功', + 'data': { + 'list': [], + 'has_more': False, + 'total': 0 + } + }) + + # 查询主表 + status_query = Q() + if zhuangtai_list != [1, 2, 3, 4, 5, 6, 7, 8]: + status_query = Q(zhuangtai__in=zhuangtai_list) + + query = Q(dingdan_id__in=pingtai_order_ids) & status_query + orders = Dingdan.objects.filter(query).order_by('-create_time') + #total_orders = orders.count() + + # 手动分页逻辑 + start_index = (page - 1) * page_size + end_index = start_index + page_size + + # 获取当前页的数据 + current_page_orders = orders[start_index:end_index] + + # 构建返回数据 + order_list = [] + for order in current_page_orders: + order_data = { + 'dingdanId': order.dingdan_id, + 'zhuangtai': order.zhuangtai, + 'jine': float(order.jine) if order.jine else 0.0, + 'tupian': order.tupian or '', + 'jieshao': order.jieshao or '', + 'beizhu': order.beizhu or '', + 'nicheng': order.nicheng or '', + 'chuangjianshijian': order.create_time.strftime('%Y-%m-%d %H:%M:%S') + } + order_list.append(order_data) + + + # 判断是否还有更多数据 + current_page_count = len(current_page_orders) + has_more = current_page_count == page_size + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': { + 'list': order_list, + 'has_more': has_more, + #'total': total_orders + } + }) + + except Exception as e: + return Response({ + 'code': 4, + 'msg': '服务器内部错误', + 'data': None + }) + + +# 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): + """订单详情接口""" + permission_classes = [IsAuthenticated] + + def post(self, request): + # 获取当前登录用户 + try: + current_user = request.user + yonghuid = current_user.yonghuid + except Exception as e: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }) + + # 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdanId', '') + except Exception as e: + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }) + + # 参数验证 + if not dingdan_id: + return Response({ + 'code': 3, + 'msg': '订单ID不能为空', + 'data': None + }) + + try: + # 1. 验证订单是否属于当前用户 + # 查询平台订单扩展表,检查laoban_id是否等于当前用户 + pingtai_kuozhan = DingdanPingtai.objects.filter( + laoban_id=yonghuid, + dingdan__dingdan_id=dingdan_id + ).first() + + if not pingtai_kuozhan: + # 如果不是平台订单,检查商家订单 + from .models import DingdanShangjia + shangjia_kuozhan = DingdanShangjia.objects.filter( + shangjia_id=yonghuid, + dingdan__dingdan_id=dingdan_id + ).first() + + if not shangjia_kuozhan: + return Response({ + 'code': 4, + 'msg': '订单不存在或无权限查看', + 'data': None + }) + + # 2. 查询订单主表 + dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + + if not dingdan: + return Response({ + 'code': 5, + 'msg': '订单不存在', + 'data': None + }) + + # 3. 获取订单信息 + zhuangtai = dingdan.zhuangtai or 0 + zhiding_id = dingdan.zhiding_id or '' + jiedan_dashou_id = dingdan.jiedan_dashou_id or '' + dashou_liuyan = dingdan.dashou_liuyan or '' + + # 4. 查询打手交付图片 + dashoutupian_records = Dashoutupian.objects.filter( + dingdan_id=dingdan_id + ).order_by('create_time')[:9] # 最多取9条 + + dashoujiaofu_list = [] + for record in dashoutupian_records: + if record.tupian: + dashoujiaofu_list.append(record.tupian) + + # 5. 构建返回数据(字段名与前端对应) + response_data = { + 'zhuangtai': zhuangtai, # 订单状态 + 'zhiding': zhiding_id, # 指定打手ID + 'dashou': jiedan_dashou_id, # 服务打手ID + 'dashouliuyan': dashou_liuyan, # 打手留言 + 'dashoujiaofu': dashoujiaofu_list # 打手交付图片列表 + } + + # 6. 返回成功响应 + return Response({ + 'code': 0, + 'msg': '成功', + 'data': response_data + }) + + except Exception as e: + print(f"订单详情查询失败: {str(e)}") + return Response({ + 'code': 6, + 'msg': '服务器内部错误', + 'data': None + }) + + +class JiedanView(APIView): + """结单接口""" + permission_classes = [IsAuthenticated] + + def post(self, request): + # 获取当前登录用户 + try: + current_user = request.user + yonghuid = current_user.yonghuid + except Exception as e: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }) + + # 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdanId', '') + pingfen = int(data.get('pingfen', 0)) # 评分,默认0 + liuyan = data.get('liuyan', '') # 留言,默认空 + except Exception as e: + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }) + + # 参数验证 + if not dingdan_id: + return Response({ + 'code': 3, + 'msg': '订单ID不能为空', + 'data': None + }) + + # 验证评分范围 + if pingfen < 0 or pingfen > 5: + return Response({ + 'code': 4, + 'msg': '评分必须在0-5分之间', + 'data': None + }) + + # 使用事务确保数据一致性 + try: + with transaction.atomic(): + # 1. 验证订单是否存在且属于当前用户 + pingtai_kuozhan = DingdanPingtai.objects.filter( + laoban_id=yonghuid, + dingdan__dingdan_id=dingdan_id + ).select_related('dingdan').first() + + if not pingtai_kuozhan: + return Response({ + 'code': 5, + 'msg': '订单不存在或无权限操作', + 'data': None + }) + + dingdan = pingtai_kuozhan.dingdan + + # 2. 验证订单状态(必须是结算中 - 状态8) + if dingdan.zhuangtai != 8: + return Response({ + 'code': 6, + 'msg': '订单不在结算状态,无法结单', + 'data': None + }) + + # 3. 获取订单相关数据 + dashou_fencheng = dingdan.dashou_fencheng or Decimal('0.00') # 打手分成 + jiedan_dashou_id = dingdan.jiedan_dashou_id or '' # 接单打手ID + shangpin_id = dingdan.shangpin_id # 商品ID + + # 4. 更新平台扩展表的老板评价(如果有留言) + if liuyan: + pingtai_kuozhan.laoban_pingjia = liuyan + pingtai_kuozhan.save() + + # 5. 如果有评分,更新评分表 + if pingfen > 0 and jiedan_dashou_id: + # 使用filter查询,更健壮 + pingfen_records = Pingfen.objects.filter(dashou=jiedan_dashou_id) + + if pingfen_records.exists(): + # 已有记录,更新 + pingfen_record = pingfen_records.first() + pingfen_record.zongfen += Decimal(str(pingfen)) + pingfen_record.cishu += 1 + pingfen_record.save() + else: + # 新记录,创建 + Pingfen.objects.create( + dashou=jiedan_dashou_id, + zongfen=Decimal(str(pingfen)), + cishu=1 + ) + + # 6. 更新商品真实销量(如果有商品ID) + if shangpin_id: + shangpin_records = Shangpin.objects.filter(id=shangpin_id) + + if shangpin_records.exists(): + shangpin = shangpin_records.first() + # 检查字段是否存在 + if hasattr(shangpin, 'zhenshi_xiaoliang'): + shangpin.zhenshi_xiaoliang = (shangpin.zhenshi_xiaoliang or 0) + 1 + shangpin.save() + + # ========== 新增:跨平台我方派单且有对方信息,直接通知对方并跳过本地结算 ========== + if (dingdan.is_cross == 1 + and dingdan.partner_order_id + and dingdan.partner_club_id): + # 更新订单状态为已完成 + dingdan.zhuangtai = 3 + dingdan.clkf = current_user.phone if hasattr(current_user, 'phone') else yonghuid + dingdan.save() + + # 事务提交后异步通知对方平台 + transaction.on_commit( + lambda: self._notify_partner_jiedan( + my_dingdan_id=dingdan.dingdan_id, + partner_club_id=dingdan.partner_club_id, + partner_order_id=dingdan.partner_order_id, + new_status=3, + current_user=current_user + ) + ) + + # 直接返回,跳过后续所有本地打手结算、评价、统计等 + return Response({ + 'code': 0, + 'msg': '结单成功(已通知对方平台)', + 'data': None + }) + # ==================================================================== + + # 7. 更新打手扩展表(如果有打手ID和分成) + if jiedan_dashou_id and dashou_fencheng > 0: + # 查询打手用户 + dashou_user = UserMain.objects.filter(yonghuid=jiedan_dashou_id).first() + + if dashou_user: + # 查询打手扩展表 + dashou_kuozhan = UserDashou.objects.filter(user=dashou_user).first() + + if dashou_kuozhan: + # 更新四个金额字段 + dashou_kuozhan.zonge = (dashou_kuozhan.zonge or Decimal('0.00')) + dashou_fencheng + dashou_kuozhan.yue = (dashou_kuozhan.yue or Decimal('0.00')) + dashou_fencheng + dashou_kuozhan.jinrishouyi = (dashou_kuozhan.jinrishouyi or Decimal( + '0.00')) + dashou_fencheng + dashou_kuozhan.jinyueshouyi = (dashou_kuozhan.jinyueshouyi or Decimal( + '0.00')) + dashou_fencheng + + # 成交总量加1 + dashou_kuozhan.chengjiaozongliang = (dashou_kuozhan.chengjiaozongliang or 0) + 1 + + # 状态改为数字1 + dashou_kuozhan.zhuangtai = 1 + + dashou_kuozhan.save() + + # 8. 更新订单状态为已完成(状态3) + dingdan.zhuangtai = 3 + dingdan.save() + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=jiedan_dashou_id, + amount=dashou_fencheng, + action=2 # 2 表示成交(结算) + ) + logger.info(f"打手 {jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + + + # ========== 新增:商品和店铺每日统计 ========== + try: + # 商品每日统计(只要有商品ID就执行) + if dingdan.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(dingdan, 'pingtai_kuozhan'): + pingtai_shouyi = dingdan.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=dingdan.shangpin_id, + caozuo_leixing=2, # 2 = 结单 + dingdan_jiage=dingdan.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {dingdan.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + + + # ========== 修改为 ========== + # 店铺每日统计(包含上级分红) + dianpu_id = None + shangji_id = None + shangji_fen = Decimal('0.00') + if hasattr(dingdan, 'pingtai_kuozhan'): + dianpu_id = dingdan.pingtai_kuozhan.dianpu_id + shangji_id = dingdan.pingtai_kuozhan.shangji_dianpu_id + shangji_fen = dingdan.pingtai_kuozhan.shangji_fenhong or Decimal('0.00') + if dianpu_id: + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=2, # 结算 + dingdan_jiage=dingdan.jine, + dianpu_shouyi=dianpu_shouyi, + shangji_dianpu_id=shangji_id, # 新增:上级店铺ID + shangji_fenhong=shangji_fen # 新增:该单给上级的分红 + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}, 上级ID: {shangji_id}, 上级分红: {shangji_fen}") + + + '''dianpu_id = None + if hasattr(dingdan, 'pingtai_kuozhan'): + dianpu_id = dingdan.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = dingdan.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=2, # 2 = 结单 + dingdan_jiage=dingdan.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")''' + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + # 12. 更新平台收益流水(仅在平台发单时) + if dingdan.fadan_pingtai == 1: + try: + from dingdan.utils import update_platform_profit # 导入函数 + update_platform_profit(dingdan_id) + except Exception as e: + # 平台收益更新失败不影响主流程,但记录日志 + logger.error(f"强制结单后更新平台收益失败: {str(e)}") + + # 9. 返回成功响应 + return Response({ + 'code': 0, + 'msg': '结单成功', + 'data': None + }) + + except Exception as e: + + # 事务会自动回滚 + return Response({ + 'code': 7, + 'msg': '结单失败,服务器内部错误', + 'data': None + }) + + + def _notify_partner_jiedan(self, my_dingdan_id, partner_club_id, partner_order_id, new_status, current_user): + """ + 通知对方俱乐部我方订单已完成 + 参照 CrossPlatformForceCompleteView._notify_partner_order_status 实现 + """ + try: + # 1. 获取我方俱乐部ID + from .models import CrossPlatformOrderData + 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 + + # 2. 查询对方服务器域名 + 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 + + # 3. 构造通知请求 + url = partner_domain.rstrip('/') + '/houtai/dfddtk' + payload = { + 'order_id': partner_order_id, # 在对方看来这是他们的订单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: + # 4. 更新跨平台订单扩展表(记录对方状态) + CrossPlatformOrderData.objects.update_or_create( + dingdan_id=my_dingdan_id, # 我方订单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') + } + ) + 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)}") + + +from dengji.models import DashouBiaoxian +from dengji.services import record_dashou_biaoxian + + +class DingdanXiangqingView2(APIView): + """订单详情接口(老板端)- 返回完整评价信息""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghuid = request.user.yonghuid + except Exception: + return Response({'code': 1, 'msg': '用户认证失败', 'data': None}) + + dingdan_id = request.data.get('dingdanId', '') + if not dingdan_id: + return Response({'code': 2, 'msg': '订单ID不能为空', 'data': None}) + + try: + # 权限验证 + pingtai_ext = DingdanPingtai.objects.filter( + laoban_id=yonghuid, + dingdan__dingdan_id=dingdan_id + ).select_related('dingdan').first() + + if not pingtai_ext: + return Response({'code': 3, 'msg': '订单不存在或无权限查看', 'data': None}) + + order = pingtai_ext.dingdan + zhuangtai = order.zhuangtai or 0 + jiedan_dashou_id = order.jiedan_dashou_id or '' + dashou_liuyan = order.dashou_liuyan or '' + update_time = order.update_time.strftime('%Y-%m-%d %H:%M:%S') if order.update_time else '' + + # 交付图片 + dashoujiaofu_list = list( + Dashoutupian.objects.filter(dingdan_id=dingdan_id) + .order_by('create_time')[:9] + .values_list('tupian', flat=True) + ) + + # 打手信息 + dashou_nicheng = '' + dashou_touxiang = '' + jiedan_time = '' + tijiao_time = '' + if jiedan_dashou_id: + dashou_user = UserMain.objects.filter(yonghuid=jiedan_dashou_id).first() + if dashou_user: + dashou_touxiang = dashou_user.avatar or '' + try: + dashou_nicheng = dashou_user.dashou_profile.nicheng or '' + except UserDashou.DoesNotExist: + pass + biaoxian = DashouBiaoxian.objects.filter( + dingdan_id=dingdan_id, + dashou_id=jiedan_dashou_id + ).first() + if biaoxian: + if biaoxian.jiedan_time: + jiedan_time = biaoxian.jiedan_time.strftime('%Y-%m-%d %H:%M:%S') + if biaoxian.tijiao_time: + tijiao_time = biaoxian.tijiao_time.strftime('%Y-%m-%d %H:%M:%S') + + # 老板自己的评价(如果已经结算过,可能已有评价) + laoban_pingjia = pingtai_ext.laoban_pingjia or '' + shangxian_sudu = None + fuwu_taidu = None + fanche_cishu = None + if jiedan_dashou_id and zhuangtai in [3, 4, 5, 6]: # 已完成或退款相关状态才可能有评价 + biaoxian = DashouBiaoxian.objects.filter( + dingdan_id=dingdan_id, + dashou_id=jiedan_dashou_id + ).first() + if biaoxian: + shangxian_sudu = biaoxian.shangxian_sudu + fuwu_taidu = biaoxian.fuwu_taidu + fanche_cishu = biaoxian.fanche_cishu + + data = { + 'zhuangtai': zhuangtai, + 'dashou': jiedan_dashou_id, + 'dashou_nicheng': dashou_nicheng, + 'dashou_touxiang': dashou_touxiang, + 'dashouliuyan': dashou_liuyan, + 'dashoujiaofu': dashoujiaofu_list, + 'update_time': update_time, + 'jiedan_time': jiedan_time, + 'tijiao_time': tijiao_time, + 'laoban_pingjia': laoban_pingjia, + 'shangxian_sudu': shangxian_sudu, # 1-5 或 null + 'fuwu_taidu': fuwu_taidu, # 1-5 或 null + 'fanche_cishu': fanche_cishu # int 或 null + } + + return Response({'code': 0, 'msg': '成功', 'data': data}) + + except Exception as e: + logger.error(f"订单详情查询异常: {e}", exc_info=True) + return Response({'code': 4, 'msg': '服务器内部错误', 'data': None}) + + +class JiedanView2(APIView): + """结单接口(老板端)- 接收全新评价字段并写入表现表""" + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 用户认证 + try: + current_user = request.user + yonghuid = current_user.yonghuid + except Exception: + return Response({'code': 1, 'msg': '用户认证失败', 'data': None}) + + # 2. 获取参数(字段名与前端完全一致) + try: + data = request.data + dingdan_id = data.get('dingdanId', '') + fanche_cishu = data.get('fanche_cishu') # 前端传的字符串或数字,后端转为整数 + shangxian_sudu = data.get('shangxian_sudu') + fuwu_taidu = data.get('fuwu_taidu') + liuyan = data.get('liuyan', '') + except Exception: + return Response({'code': 2, 'msg': '参数格式错误', 'data': None}) + + # 参数校验 + if not dingdan_id: + return Response({'code': 3, 'msg': '订单ID不能为空', 'data': None}) + + # 转换并校验必填评价字段 + try: + fanche_cishu = int(fanche_cishu) if fanche_cishu is not None else None + shangxian_sudu = int(shangxian_sudu) if shangxian_sudu is not None else None + fuwu_taidu = int(fuwu_taidu) if fuwu_taidu is not None else None + except (ValueError, TypeError): + return Response({'code': 4, 'msg': '评价参数必须为整数', 'data': None}) + + if fanche_cishu is None: + return Response({'code': 5, 'msg': '翻车次数不能为空', 'data': None}) + if not (1 <= shangxian_sudu <= 5 and 1 <= fuwu_taidu <= 5): + return Response({'code': 6, 'msg': '评价分数必须在1-5之间', 'data': None}) + + # 使用事务保证数据一致性 + try: + with transaction.atomic(): + # 3. 权限验证:订单属于当前老板 + pingtai_ext = DingdanPingtai.objects.filter( + laoban_id=yonghuid, + dingdan__dingdan_id=dingdan_id + ).select_related('dingdan').first() + + if not pingtai_ext: + return Response({'code': 7, 'msg': '订单不存在或无权限操作', 'data': None}) + + order = pingtai_ext.dingdan + + # 4. 状态校验:必须是结算中(状态8) + if order.zhuangtai != 8: + return Response({'code': 8, 'msg': '订单不在结算状态', 'data': None}) + + # 5. 提取订单关键数据 + dashou_fencheng = order.dashou_fencheng or Decimal('0.00') + jiedan_dashou_id = order.jiedan_dashou_id or '' + shangpin_id = order.shangpin_id + + # 6. 保存老板评价(兼容旧逻辑) + if liuyan: + pingtai_ext.laoban_pingjia = liuyan + pingtai_ext.save() + + # 7. 更新评分表(用服务态度分作为综合评分,保留原有统计) + if jiedan_dashou_id and fuwu_taidu: + pingfen_records = Pingfen.objects.filter(dashou=jiedan_dashou_id) + if pingfen_records.exists(): + pf = pingfen_records.first() + pf.zongfen += Decimal(str(fuwu_taidu)) + pf.cishu += 1 + pf.save() + else: + Pingfen.objects.create( + dashou=jiedan_dashou_id, + zongfen=Decimal(str(fuwu_taidu)), + cishu=1 + ) + + # 8. 更新商品真实销量 + if shangpin_id: + shangpin_qs = Shangpin.objects.filter(id=shangpin_id) + if shangpin_qs.exists(): + sp = shangpin_qs.first() + if hasattr(sp, 'zhenshi_xiaoliang'): + sp.zhenshi_xiaoliang = (sp.zhenshi_xiaoliang or 0) + 1 + sp.save() + + # 9. 更新打手收益与状态 + if jiedan_dashou_id and dashou_fencheng > 0: + dashou_user = UserMain.objects.filter(yonghuid=jiedan_dashou_id).first() + if dashou_user: + try: + dashou_ext = dashou_user.dashou_profile + dashou_ext.zonge = (dashou_ext.zonge or Decimal('0.00')) + dashou_fencheng + dashou_ext.yue = (dashou_ext.yue or Decimal('0.00')) + dashou_fencheng + dashou_ext.jinrishouyi = (dashou_ext.jinrishouyi or Decimal('0.00')) + dashou_fencheng + dashou_ext.jinyueshouyi = (dashou_ext.jinyueshouyi or Decimal('0.00')) + dashou_fencheng + dashou_ext.chengjiaozongliang = (dashou_ext.chengjiaozongliang or 0) + 1 + dashou_ext.zhuangtai = 1 + dashou_ext.save() + except UserDashou.DoesNotExist: + pass + + # 10. 更新订单状态为已完成 + order.zhuangtai = 3 + order.save() + + # 11. 打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=jiedan_dashou_id, + amount=dashou_fencheng, + action=2 # 2=结算/成交 + ) + except Exception as e: + logger.error(f"打手每日统计失败: {e}") + + # 12. 商品/店铺每日统计 + try: + if 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=shangpin_id, + caozuo_leixing=2, + dingdan_jiage=order.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + + # 店铺统计(含上级分红) + if hasattr(order, 'pingtai_kuozhan'): + dianpu_id = order.pingtai_kuozhan.dianpu_id + shangji_id = order.pingtai_kuozhan.shangji_dianpu_id + shangji_fen = order.pingtai_kuozhan.shangji_fenhong or Decimal('0.00') + if dianpu_id: + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=2, + dingdan_jiage=order.jine, + dianpu_shouyi=dianpu_shouyi, + shangji_dianpu_id=shangji_id, + shangji_fenhong=shangji_fen + ) + except Exception as e: + logger.error(f"商品/店铺统计失败: {e}") + + # 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}") + + # 14. 🆕 写入打手表现表(结算评价) + if jiedan_dashou_id: + record_dashou_biaoxian( + dingdan_id=dingdan_id, + dashou_id=jiedan_dashou_id, + xingwei=3, # 3=结算 + fanche_cishu=fanche_cishu, + shangxian_sudu=shangxian_sudu, + fuwu_taidu=fuwu_taidu + # bankuai 暂不传入,后续可从订单商品关联 + ) + + # 15. 返回成功 + return Response({'code': 0, 'msg': '结单成功', 'data': None}) + + except Exception as e: + logger.error(f"结单事务异常: {e}", exc_info=True) + 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): + """ + 获取商家可用的商品类型列表(含关联板块的称号) + POST /dingdan/sjspleixing + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + + try: + # 1. 查询审核通过且抢单要求类型为1的商品类型,按排序权重升序 + shangpin_queryset = ShangpinLeixing.objects.filter( + yaoqiuleixing=1, + shenhezhuangtai=1 + ).select_related('bankuai').order_by('paixu') + + # 2. 收集所有涉及到的板块ID + bankuai_ids = shangpin_queryset.values_list('bankuai_id', flat=True).distinct() + + # 3. 查询这些板块下的所有称号(每个称号绑定到一个板块) + chenghao_list = Chenghao.objects.filter( + bankuai_id__in=bankuai_ids + ).values('id', 'mingcheng', 'texiao_miaoshu', 'bankuai_id') + + # 4. 构建 板块ID -> 称号列表 的映射(确保准确关联) + bankuai_chenghao_map = {} + for ch in chenghao_list: + bid = ch['bankuai_id'] + if bid not in bankuai_chenghao_map: + bankuai_chenghao_map[bid] = [] + bankuai_chenghao_map[bid].append({ + 'id': ch['id'], + 'mingcheng': ch['mingcheng'], + 'texiao_json': ch['texiao_miaoshu'] # 前端 chenghao-tag 组件需要 + }) + + # 5. 组装最终数据 + data_list = [] + for sp in shangpin_queryset: + item = { + 'id': sp.id, + 'yaoqiuleixing': sp.yaoqiuleixing, + 'huiyuan_id': sp.huiyuan_id, + 'jieshao': sp.jieshao, + # 当前类型所属板块的称号列表(无板块则为空数组) + 'chenghaoList': bankuai_chenghao_map.get(sp.bankuai_id, []) + } + data_list.append(item) + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': data_list + }) + + 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] + + 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, 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_yongjin = None + 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: + final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1 + final_huiyuan_id = str(huiyuan_id) if huiyuan_id else '' + logger.warning("佣金为0,降级为会员模式") + except ValueError: + final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1 + final_huiyuan_id = str(huiyuan_id) if huiyuan_id else '' + logger.warning("佣金格式错误,降级为会员模式") + else: + final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1 + final_huiyuan_id = str(huiyuan_id) if huiyuan_id else '' + logger.info(f"会员模式: 类型={final_yaoqiu_type}") + + # ----- 7. 称号(可选:不传/空值/无效/不存在均视为无标签)----- + chenghao_obj = None + label_id_str = str(label_id).strip() if label_id is not None else '' + if label_id_str and label_id_str not in ('0', 'null', 'undefined'): + try: + lid = int(label_id_str) + if lid > 0: + chenghao_obj = Chenghao.objects.filter(id=lid).first() + if chenghao_obj: + logger.info(f"订单将关联称号: {chenghao_obj.mingcheng}") + else: + logger.info(f"标签不存在,按无标签派单: {label_id}") + except (ValueError, TypeError): + logger.info(f"标签ID无效,按无标签派单: {label_id}") + + # ----- 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. 利率(打手+管事分成,打手优先) ----- + 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) + 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, + guanshi_fencheng=guanshi_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) + + # Celery 广播任务(异步) + 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 '游戏订单' + +# 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): + """ + 商家订单获取接口 + POST /dingdan/sjdingdanhq + 需要JWT认证 + 支持分页、状态筛选、商品类型筛选、模糊搜索(订单ID/订单介绍/游戏昵称/打手ID) + 返回待结算订单数量(pending_count) + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取当前登录用户的yonghuid + try: + current_user = request.user + yonghuid = current_user.yonghuid + except AttributeError: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 解析参数 + try: + data = request.data + zhuangtai_list = data.get('zhuangtai_list', []) + page = max(1, int(data.get('page', 1))) + page_size = min(100, max(1, int(data.get('page_size', 10)))) + + # 🆕 商品类型筛选 + leixing_id = data.get('leixing_id', None) + + # 🆕 模糊搜索关键字(支持订单ID、订单介绍、游戏昵称、打手ID) + keyword = data.get('keyword', '').strip() + except (ValueError, TypeError): + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 验证状态列表 + if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0: + return Response({ + 'code': 3, + 'msg': '状态列表不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 4. 构建基础查询(该商家的所有订单) + base_qs = DingdanShangjia.objects.filter( + shangjia_id=yonghuid + ).select_related('dingdan') + + # 5. 状态过滤 + base_qs = base_qs.filter(dingdan__zhuangtai__in=zhuangtai_list) + + # 🆕 6. 商品类型筛选 + if leixing_id is not None: + try: + leixing_id_int = int(leixing_id) + base_qs = base_qs.filter(dingdan__leixing_id=leixing_id_int) + except (ValueError, TypeError): + # 如果leixing_id不是有效整数,忽略该筛选 + pass + + # 🆕 7. 模糊搜索(支持订单ID、订单介绍、游戏昵称、打手ID) + if keyword: + from django.db.models import Q + # 订单ID精确匹配(dingdan_id有唯一索引,先用精确匹配) + dingdan_id_match = Q(dingdan__dingdan_id__exact=keyword) + # 订单介绍模糊匹配 + jieshao_match = Q(dingdan__jieshao__icontains=keyword) + # 游戏昵称模糊匹配 + nicheng_match = Q(dingdan__nicheng__icontains=keyword) + # 打手ID精确匹配 + dashou_match = Q(dingdan__jiedan_dashou_id__exact=keyword) + + base_qs = base_qs.filter( + dingdan_id_match | jieshao_match | nicheng_match | dashou_match + ) + + # 8. 计算待结算订单数量(状态为8) + pending_count = DingdanShangjia.objects.filter( + shangjia_id=yonghuid, + dingdan__zhuangtai=8 + ).count() + + # 9. 排序、分页 + total_count = base_qs.count() + start_index = (page - 1) * page_size + end_index = start_index + page_size + current_page_orders = base_qs.order_by('-dingdan__create_time')[start_index:end_index] + + # 10. 构建返回数据 + order_list = [] + for shangjia_order in current_page_orders: + order = shangjia_order.dingdan + order_data = { + 'dingdan_id': order.dingdan_id, + 'zhuangtai': order.zhuangtai, + 'jine': float(order.jine) if order.jine else 0.0, + 'jieshao': order.jieshao or '', + 'beizhu': order.beizhu or '', + 'nicheng': order.nicheng or '', + 'leixing_id': order.leixing_id, # 🆕 返回商品类型ID + 'tupian': order.tupian or '', # 🆕 返回商品图片 + 'jiedan_dashou_id': order.jiedan_dashou_id or '', # 🆕 返回打手ID + 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '' + } + order_list.append(order_data) + + # 11. 判断是否还有更多 + current_page_count = len(current_page_orders) + has_more = current_page_count == page_size and end_index < total_count + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': { + 'list': order_list, + 'has_more': has_more, + 'total': total_count, + 'pending_count': pending_count + } + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"商家订单获取失败: {str(e)}", exc_info=True) + return Response({ + 'code': 4, + 'msg': '服务器内部错误', + 'data': None + }, 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): + """ + 商家订单详情接口 + 返回订单基础信息、打手信息、交付图片、罚款数据、分红利率、更换打手产生的新订单ID等 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取当前登录用户的 yonghuid + try: + current_user = request.user + yonghuid = current_user.yonghuid + except AttributeError: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdan_id', '').strip() + except Exception: + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not dingdan_id: + return Response({ + 'code': 3, + 'msg': '订单ID不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 3. 查询商家订单扩展表,验证权限并获取基础订单数据 + shangjia_kuozhan = DingdanShangjia.objects.filter( + shangjia_id=yonghuid, + dingdan__dingdan_id=dingdan_id + ).select_related('dingdan').first() + + if not shangjia_kuozhan: + return Response({ + 'code': 4, + 'msg': '订单不存在或您无权限查看', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + order = shangjia_kuozhan.dingdan + + # 4. 交付图片列表 + dashou_tupian_list = Dashoutupian.objects.filter( + dingdan_id=dingdan_id + ).order_by('create_time').values_list('tupian', flat=True) + dashou_jiaofu = list(dashou_tupian_list) + + # 5. 打手详细信息(头像、昵称、用户ID) + dashou_yonghuid = order.jiedan_dashou_id or '' + dashou_nicheng = '' + dashou_avatar = '' + if dashou_yonghuid: + dashou_user = UserMain.objects.filter( + yonghuid=dashou_yonghuid + ).select_related('dashou_profile').first() + if dashou_user: + dashou_avatar = dashou_user.avatar or '' + if hasattr(dashou_user, 'dashou_profile') and dashou_user.dashou_profile: + dashou_nicheng = dashou_user.dashou_profile.nicheng or '' + + # 6. 罚款数据(如果存在) + fadan_obj = Fadan.objects.filter( + guanliandingdan_id=dingdan_id, + beichufa_id=dashou_yonghuid + ).first() + fadan_dict = None + if fadan_obj: + fadan_dict = { + 'id': fadan_obj.id, + 'chufaliyou': fadan_obj.chufaliyou or '', + 'fakuanjine': float(fadan_obj.fakuanjine) if fadan_obj.fakuanjine else 0.0, + 'yingxiang_qiangdan': fadan_obj.yingxiang_qiangdan, + 'zhuangtai': fadan_obj.zhuangtai, + 'shensuliyou': fadan_obj.shensuliyou or '', + 'tongyi_chufaliyou': fadan_obj.tongyi_chufaliyou or '', + 'bohuiliyou': fadan_obj.bohuiliyou or '', + 'create_time': fadan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if fadan_obj.create_time else '', + 'update_time': fadan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if fadan_obj.update_time else '', + } + + # 7. 商家自己的分红利率(用于罚款分红计算) + fenhong_lilv = 0.0 + try: + # 查找商家作为分红者的最新记录,取应得利率 + last_fenhong = FadanFenhong.objects.filter( + fenhongzhe_id=yonghuid, + shenfen=1 # 1=商家 + ).order_by('-create_time').first() + if last_fenhong: + fenhong_lilv = float(last_fenhong.yingde_lilv or 0) + except Exception as e: + logger.warning(f'获取分红利率失败: {e}') + + # 8. 更换打手后产生的新订单ID(如果当前订单被更换过) + xin_dingdan_id = '' + try: + # 查找以当前订单作为“上一个订单ID”的新订单扩展记录 + new_shangjia_order = DingdanShangjia.objects.filter( + shangyige_dingdan_id=dingdan_id, + shangjia_id=yonghuid + ).select_related('dingdan').first() + if new_shangjia_order: + xin_dingdan_id = new_shangjia_order.dingdan.dingdan_id + except Exception as e: + logger.warning(f'查找新订单ID失败: {e}') + + # 9. 构建返回数据 + response_data = { + # 基础订单字段 + 'dingdan_id': order.dingdan_id, + 'zhuangtai': order.zhuangtai or 0, + 'jine': float(order.jine) if order.jine else 0.0, + 'jieshao': order.jieshao or '', + 'beizhu': order.beizhu or '', + 'nicheng': order.nicheng or '', + 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '', + + # 打手信息 + 'dashou_yonghuid': dashou_yonghuid, + 'dashou_nicheng': dashou_nicheng, + 'dashou_avatar': dashou_avatar, + + # 服务详情 + 'fuwudashou_id': dashou_yonghuid, # 保留旧字段兼容 + 'dashou_liuyan': order.dashou_liuyan or '', + 'dashou_jiaofu': dashou_jiaofu, + + # 商家扩展字段(旧) + 'shangjia_liuyan': shangjia_kuozhan.shangjia_pingjia or '', + 'tkliyou': order.tkly or '', + 'cfliyou': shangjia_kuozhan.cfliyou or '', # 保留,但不影响新罚款 + 'sqzhuangtai': shangjia_kuozhan.sqzhuangtai or 0, + 'bhliyou': shangjia_kuozhan.bhliyou or '', + 'chufa_jieguo': '', # 已弃用 + + # 新增:罚款数据 + 'fadan': fadan_dict, + + # 新增:分红利率 + 'fenhong_lilv': fenhong_lilv, + + # 新增:更换打手后的新订单ID + 'xin_dingdan_id': xin_dingdan_id, + } + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': response_data + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"商家订单详情查询失败: {str(e)}", exc_info=True) + return Response({ + 'code': 5, + 'msg': '服务器内部错误', + 'data': None + }, 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] + + def post(self, request): + # 1. 获取当前登录用户 + try: + current_user = request.user + yonghuid = current_user.yonghuid + + try: + shangjia = current_user.shop_profile + except UserShangjia.DoesNotExist: + return Response({ + 'code': 1, + 'msg': '您不是商家或商家信息未完善', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + except AttributeError: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdan_id', '').strip() + pingfen = int(data.get('pingfen', 0)) + liuyan = data.get('liuyan', '').strip() + except (ValueError, TypeError): + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 参数验证 + if not dingdan_id: + return Response({ + 'code': 3, + 'msg': '订单ID不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if pingfen < 0 or pingfen > 5: + return Response({ + 'code': 4, + 'msg': '评分必须在0-5分之间', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + with transaction.atomic(): + # 4. 查询订单并加锁(修复并发问题) + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + # 验证订单是否属于当前商家 + shangjia_kuozhan = DingdanShangjia.objects.filter( + dingdan=order, + shangjia_id=yonghuid + ).first() + + if not shangjia_kuozhan: + return Response({ + 'code': 5, + 'msg': '订单不存在或您无权限操作', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 5. 验证订单状态 + if order.zhuangtai != 8: + return Response({ + 'code': 6, + 'msg': '订单不在结算状态,无法结单', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 获取订单字段 + jine = order.jine or Decimal('0') + dashou_fencheng = order.dashou_fencheng or Decimal('0') + jiedan_dashou_id = order.jiedan_dashou_id + dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单 + is_cross = order.is_cross + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + + if not jiedan_dashou_id: + return Response({ + 'code': 7, + 'msg': '订单没有接单打手,无法结单', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # ========== 跨平台判断 ========== + is_cross_claimed_by_partner = (is_cross == 1 and (partner_club_id or partner_order_id)) + + # ========== 根据订单类型处理 ========== + # 情况1:对方派单(我方打手接单)-> 预结算,不直接给打手加钱 + if dispatch_type == 2: + # 创建预结算记录 + PreSettlement.objects.update_or_create( + dingdan_id=dingdan_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id or '000001', + 'dashou_id': jiedan_dashou_id, + 'amount': dashou_fencheng, + 'order_amount': jine, + 'status': 0, + 'operator': request.user.phone, + } + ) + # 更新订单状态 + order.zhuangtai = 3 + order.save() + + # 更新统计表 + update_daily_dispatch_stat( + direction=2, + partner_club_id=partner_club_id or '000001', + success_amount=jine, + success_count=1 + ) + + # 情况2:我方派单且被对方打手接单 -> 只改状态,不给本地打手加钱 + elif dispatch_type == 1 and is_cross_claimed_by_partner: + order.zhuangtai = 3 + order.save() + + update_daily_dispatch_stat( + direction=1, + partner_club_id=partner_club_id or '000001', + success_amount=dashou_fencheng, + success_count=1 + ) + + # 情况3:本地订单 或 跨平台但自家打手接单 -> 正常结算给打手 + else: + # 查询打手信息 + try: + dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou = dashou_user.dashou_profile + except (UserMain.DoesNotExist, UserDashou.DoesNotExist): + return Response({ + 'code': 8, + 'msg': '接单打手不存在或信息不完整', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 原子更新打手数据(使用 F 表达式) + UserDashou.objects.filter(id=dashou.id).update( + chengjiaozongliang=F('chengjiaozongliang') + 1, + yue=F('yue') + dashou_fencheng, + zonge=F('zonge') + dashou_fencheng, + jinrishouyi=F('jinrishouyi') + dashou_fencheng, + jinyueshouyi=F('jinyueshouyi') + dashou_fencheng, + ) + + # 更新商家成交单量 + UserShangjia.objects.filter(id=shangjia.id).update( + chengjiao=F('chengjiao') + 1, + ) + + # 更新订单状态 + order.zhuangtai = 3 + order.save() + + # 处理评分(所有情况都处理) + if pingfen > 0: + pingfen_obj, created = Pingfen.objects.get_or_create( + dashou=jiedan_dashou_id, + defaults={'zongfen': pingfen, 'cishu': 1} + ) + if not created: + Pingfen.objects.filter(id=pingfen_obj.id).update( + zongfen=F('zongfen') + pingfen, + cishu=F('cishu') + 1, + ) + + # 更新商家订单扩展表的评价 + DingdanShangjia.objects.filter(id=shangjia_kuozhan.id).update( + shangjia_pingjia=liuyan, + ) + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=jiedan_dashou_id, + amount=dashou_fencheng, + action=2 # 2 表示成交(结算) + ) + logger.info(f"打手 {jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + + # ✅ 新方法(行为驱动,更安全) + update_shangjia_daily( + yonghuid=request.user.yonghuid, + amount=Decimal(str(jine)), + action=2 # 2 = 结算 + ) + + # 商家订单管事分红(仅 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) + + # 返回成功 + return Response({ + 'code': 0, + 'msg': '结单成功', + 'data': { + 'dingdan_id': dingdan_id, + 'new_zhuangtai': 3, + 'jine': float(jine), + 'dashou_fencheng': float(dashou_fencheng), + 'jiedan_dashou_id': jiedan_dashou_id, + 'pingfen': pingfen, + 'liuyan': liuyan + } + }, status=status.HTTP_200_OK) + + except Dingdan.DoesNotExist: + return Response({ + 'code': 5, + 'msg': '订单不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"商家结单失败: {str(e)}", exc_info=True) + return Response({ + 'code': 9, + 'msg': '服务器内部错误,结单失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu + + +class ShangjiaChexiaoView(APIView): + """商家撤销订单申请接口""" + permission_classes = [IsAuthenticated] # JWT认证 + + def post(self, request): + # 1. 获取当前登录用户并验证商家身份 + try: + current_user = request.user + # 使用反向查询获取商家扩展信息(性能最佳) + shangjia = current_user.shop_profile + except UserShangjia.DoesNotExist: + return Response({ + 'code': 1, + 'msg': '您不是商家或商家信息未完善', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 2. 检查商家账号状态 + if shangjia.zhuangtai != 1: + return Response({ + 'code': 2, + 'msg': '商家账号状态异常,无法操作', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdan_id', '').strip() + chexiao_liyou = data.get('chexiao_liyou', '').strip() + except Exception: + return Response({ + 'code': 3, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 参数验证 + if not dingdan_id or not chexiao_liyou: + return Response({ + 'code': 4, + 'msg': '订单ID和撤销原因不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 5. 使用事务确保数据一致性 + with transaction.atomic(): + # 查询订单主表 + dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + if not dingdan: + return Response({ + 'code': 5, + 'msg': '订单不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 6. 验证订单状态 + if dingdan.zhuangtai not in [1, 7]: + return Response({ + 'code': 6, + 'msg': '当前订单状态不可撤销', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 7. 验证订单所属商家(使用反向查询) + shangjia_kuozhan = DingdanShangjia.objects.filter( + dingdan__dingdan_id=dingdan_id, + shangjia_id=current_user.yonghuid + ).first() + + if not shangjia_kuozhan: + return Response({ + 'code': 7, + 'msg': '您无权撤销此订单', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 8. 创建退款记录(注意:申请状态直接设为1-成功) + Tuikuanjilu.objects.create( + dingdan_id=dingdan_id, + qingqiuid=current_user.yonghuid, + liyou=chexiao_liyou, + sqzhuangtai=1, # 直接成功 + jine=dingdan.jine, + dashou_fencheng=dingdan.dashou_fencheng, + jieshao=dingdan.jieshao, + beizhu=dingdan.beizhu, + nicheng=dingdan.nicheng, + + ) + + # 9. 更新商家扩展表数据(使用F表达式原子操作) + UserShangjia.objects.filter(id=shangjia.id).update( + yue=F('yue') + dingdan.jine, + tuikuan=F('tuikuan') + 1, + jinridingdan=F('jinridingdan') - 1, + jinyuedingdan=F('jinyuedingdan') - 1, + jinriliushui=F('jinriliushui') - dingdan.jine, + jinyueliushui=F('jinyueliushui') - dingdan.jine, + + ) + + # 10. 更新订单状态为5(已退款) + dingdan.zhuangtai = 5 + + dingdan.save() + + # ✅ 新方法(行为驱动,更安全) + from houtai.utils import update_shangjia_daily + update_shangjia_daily( + yonghuid=request.user.yonghuid, + amount=Decimal(str(dingdan.jine)), + action=3 # 3 = 退款 + ) + + return Response({ + 'code': 0, + 'msg': '撤销成功', + 'data': { + 'dingdan_id': dingdan_id, + 'new_zhuangtai': 5, + 'tuikuan_liyou': chexiao_liyou, + 'jine': float(dingdan.jine) if dingdan.jine else 0.0 + } + }, 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({ + 'code': 8, + 'msg': '服务器内部错误,撤销失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class ShangjiaTuikuanShenqingView(APIView): + """商家退款申请接口""" + permission_classes = [IsAuthenticated] # JWT认证 + + def post(self, request): + # 1. 获取当前登录用户并验证商家身份 + try: + current_user = request.user + shangjia = current_user.shop_profile + except UserShangjia.DoesNotExist: + return Response({ + 'code': 1, + 'msg': '您不是商家或商家信息未完善', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 2. 检查商家账号状态 + if shangjia.zhuangtai != 1: + return Response({ + 'code': 2, + 'msg': '商家账号状态异常,无法操作', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdan_id', '').strip() + tuikuan_liyou = data.get('tuikuan_liyou', '').strip() + except Exception: + return Response({ + 'code': 3, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 参数验证 + if not dingdan_id or not tuikuan_liyou: + return Response({ + 'code': 4, + 'msg': '订单ID和退款原因不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 5. 使用事务确保数据一致性 + with transaction.atomic(): + # 查询订单主表 + dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + if not dingdan: + return Response({ + 'code': 5, + 'msg': '订单不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 6. 验证订单状态 + if dingdan.zhuangtai not in [2, 8]: + return Response({ + 'code': 6, + 'msg': '当前订单状态不可申请退款', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 7. 验证订单所属商家 + shangjia_kuozhan = DingdanShangjia.objects.filter( + dingdan__dingdan_id=dingdan_id, + shangjia_id=current_user.yonghuid + ).first() + + if not shangjia_kuozhan: + return Response({ + 'code': 7, + 'msg': '您无权为此订单申请退款', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # ========== 8. 处理退款记录(修改点:先查询是否存在) ========== + refund_record, created = Tuikuanjilu.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'dashouid': dingdan.jiedan_dashou_id, + 'qingqiuid': current_user.yonghuid, + 'liyou': tuikuan_liyou, + 'sqzhuangtai': 0, # 待处理 + 'jine': dingdan.jine, + 'dashou_fencheng': dingdan.dashou_fencheng, + 'jieshao': dingdan.jieshao, + 'beizhu': dingdan.beizhu, + 'nicheng': dingdan.nicheng, + } + ) + if not created: + # 记录已存在,更新退款理由和申请时间 + refund_record.liyou = tuikuan_liyou + refund_record.save(update_fields=['liyou', 'update_time']) + logger.info(f"订单 {dingdan_id} 已有退款记录,已更新退款理由") + + # 9. 更新订单状态为4(退款中) + dingdan.zhuangtai = 4 + dingdan.tkly = tuikuan_liyou + dingdan.save() + + # ====== 原有功能:更新打手状态 ====== + if dingdan.jiedan_dashou_id: + try: + dashou_main = UserMain.objects.filter( + yonghuid=dingdan.jiedan_dashou_id + ).first() + if dashou_main and hasattr(dashou_main, 'dashou_profile'): + dashou_profile = dashou_main.dashou_profile + dashou_profile.zhuangtai = 1 + dashou_profile.save() + logger.info(f"退款申请成功,打手状态已更新:订单{dingdan_id},打手{dingdan.jiedan_dashou_id}状态变更为1") + except Exception as e: + logger.error(f"更新打手状态失败:{str(e)},订单{dingdan_id},打手ID{dingdan.jiedan_dashou_id}") + + # ========== 新增:如果是跨平台订单,异步通知对方平台 ========== + 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}" + payload = { + 'token': settings.CROSS_PLATFORM_TOKEN, + 'order_id': dingdan.dingdan_id + } + try: + resp = requests.post(url, json=payload, timeout=5) + if resp.status_code == 200: + data = resp.json() + if data.get('code') == 0: + logger.info(f"跨平台退款通知发送成功: {dingdan.dingdan_id}") + else: + logger.error(f"跨平台退款通知业务失败: {data}") + else: + logger.error(f"跨平台退款通知HTTP失败: {resp.status_code}") + except Exception as e: + logger.error(f"跨平台退款通知异常: {str(e)}", exc_info=True) + + thread = threading.Thread(target=send_refund_notify) + thread.daemon = True + thread.start() + logger.info(f"跨平台退款通知线程已启动: {dingdan.dingdan_id}") + # ========== 新增结束 ========== + + return Response({ + 'code': 0, + 'msg': '退款申请提交成功', + 'data': { + 'dingdan_id': dingdan_id, + 'new_zhuangtai': 4, + 'tuikuan_liyou': tuikuan_liyou, + 'jine': float(dingdan.jine) if dingdan.jine else 0.0, + 'dashou_id': dingdan.jiedan_dashou_id + } + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"商家退款申请失败: {str(e)}", exc_info=True) + return Response({ + 'code': 8, + 'msg': '服务器内部错误,申请失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class ShangjiaChufaShenqingView(APIView): + """商家处罚申请接口(已添加图片上传功能)""" + permission_classes = [IsAuthenticated] # JWT认证 + + def post(self, request): + # 1. 获取当前登录用户并验证商家身份 + try: + current_user = request.user + shangjia = current_user.shop_profile + except UserShangjia.DoesNotExist: + return Response({ + 'code': 1, + 'msg': '您不是商家或商家信息未完善', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 2. 检查商家账号状态 + if shangjia.zhuangtai != 1: + return Response({ + 'code': 2, + 'msg': '商家账号状态异常,无法操作', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 获取前端参数 + try: + data = request.data + dingdan_id = data.get('dingdan_id', '').strip() + dashou_id = data.get('dashou_id', '').strip() + chufa_liyou = data.get('chufa_liyou', '').strip() + chufa_tupian_urls = data.get('chufa_tupian_urls', []) # 🔴 新增:图片URL数组 + except Exception: + return Response({ + 'code': 3, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 参数验证 + if not dingdan_id or not dashou_id or not chufa_liyou: + return Response({ + 'code': 4, + 'msg': '订单ID、打手ID和处罚原因不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # 5. 使用事务确保数据一致性 + with transaction.atomic(): + # 验证打手身份 + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou = dashou_user.dashou_profile + except UserMain.DoesNotExist: + return Response({ + 'code': 5, + 'msg': '打手用户不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + except UserDashou.DoesNotExist: + return Response({ + 'code': 5, + 'msg': '该用户不是打手', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 查询订单主表 + dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + if not dingdan: + return Response({ + 'code': 6, + 'msg': '订单不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 验证订单状态 + if dingdan.zhuangtai not in [2, 8, 4, 5, 6, 3]: + return Response({ + 'code': 7, + 'msg': '当前订单状态不可申请处罚', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证订单所属商家 + shangjia_kuozhan = DingdanShangjia.objects.filter( + dingdan__dingdan_id=dingdan_id, + shangjia_id=current_user.yonghuid + ).first() + + if not shangjia_kuozhan: + return Response({ + 'code': 8, + 'msg': '您无权为此订单申请处罚', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 验证订单的打手是否与提供的打手ID一致 + if dingdan.jiedan_dashou_id != dashou_id: + return Response({ + 'code': 9, + 'msg': '该打手并非此订单的服务打手', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 创建处罚记录 + chufajilu = Chufajilu.objects.create( + dashouid=dashou_id, + qingqiuid=current_user.yonghuid, + cfliyou=chufa_liyou, + sqzhuangtai=0, # 待处理 + jifen=5, # 扣除5积分 + dingdan_id=dingdan_id, + ) + # 🔴【新增】在创建处罚记录后立即扣除打手积分 + # 注意:这里使用前面已经获取的dashou对象(打手扩展表) + jifen_kouchu = chufajilu.jifen # 扣除的积分,这里是5 + if jifen_kouchu > 0: + # 检查打手积分是否足够 + if dashou.jifen < jifen_kouchu: + dashou.jifen = 0 + else: + dashou.jifen -= jifen_kouchu + dashou.save() + + # 🔴【新增】保存处罚图片到数据库 + if chufa_tupian_urls and isinstance(chufa_tupian_urls, list): + for tupian_url in chufa_tupian_urls: + if tupian_url and tupian_url.strip(): + Chufatupian.objects.create( + dingdan_id=dingdan_id, + yonghuid=current_user.yonghuid, + tupian=tupian_url + ) + + # 更新商家订单扩展表的处罚相关字段 + shangjia_kuozhan.cfliyou = chufa_liyou + shangjia_kuozhan.sqzhuangtai = 0 + shangjia_kuozhan.save() + + return Response({ + 'code': 0, + 'msg': '处罚申请提交成功', + 'data': { + 'dingdan_id': dingdan_id, + 'dashou_id': dashou_id, + 'chufa_liyou': chufa_liyou, + 'sqzhuangtai': 0, + 'jifen': 5, + 'tupian_count': len(chufa_tupian_urls) if chufa_tupian_urls else 0 # 🔴 返回图片数量 + } + }, 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({ + 'code': 10, + 'msg': '服务器内部错误,申请失败', + 'data': None + }, 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): + """ + 获取可抢订单列表(支持标签筛选) + POST /dingdan/ddhq + 需要JWT Token认证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取请求参数 + data = request.data + leixing_id = data.get('leixing_id') + page = int(data.get('page', 1)) + page_size = int(data.get('page_size', 10)) + biaoqian_id = data.get('biaoqian_id') + + # 2. 构建基础查询条件 + base_query = Q(zhuangtai__in=[1, 7]) + if leixing_id: + base_query &= Q(leixing_id=leixing_id) + + # 3. 标签筛选:先查出符合条件的订单ID,避免跨表 JOIN 排序冲突 + if biaoqian_id and int(biaoqian_id) > 0: + biaoqian_order_ids = DingdanBiaoqian.objects.filter( + chenghao_id=int(biaoqian_id) + ).values_list('dingdan_id', flat=True) + base_query &= Q(dingdan_id__in=list(biaoqian_order_ids)) + + # 4. 构建基础查询集 + base_qs = Dingdan.objects.filter(base_query).order_by('-create_time').distinct() + + # 5. 获取总记录数 + total = base_qs.count() + + # 6. 计算分页偏移量 + offset = (page - 1) * page_size + limit = page_size + + # 7. 获取当前页数据 + page_qs = base_qs[offset:offset + limit] + + # 8. 使用子查询获取商家昵称 + shangjia_sub = DingdanShangjia.objects.filter( + dingdan__dingdan_id=OuterRef('dingdan_id') + ).values('sjnicheng')[:1] + + # 9. 获取当前页所有需要的字段 + dingdan_values = page_qs.annotate( + sjnicheng=Subquery(shangjia_sub) + ).values( + 'dingdan_id', 'zhuangtai', 'fadan_pingtai', 'dashou_fencheng', + 'zhiding_id', 'leixing_id', 'tupian', 'yaoqiuleixing', + 'huiyuan_id', 'yongjin', 'jieshao', 'beizhu', 'create_time', + 'sjnicheng' + ) + + # 10. 转换为列表 + dingdan_list = list(dingdan_values) + + # 11. 批量获取指定打手的头像和昵称 + zhiding_ids = {item['zhiding_id'] for item in dingdan_list if item.get('zhiding_id')} + zhiding_info = {} + if zhiding_ids: + dashou_query = UserMain.objects.filter( + yonghuid__in=zhiding_ids + ).select_related('dashou_profile').annotate( + nicheng=F('dashou_profile__nicheng') + ).values('yonghuid', 'avatar', 'nicheng') + zhiding_info = { + item['yonghuid']: { + 'avatar': item['avatar'] or '', + 'nicheng': item['nicheng'] or '' + } + for item in dashou_query + } + + # ============ 批量获取三层标签 ============ + order_ids = [item['dingdan_id'] for item in dingdan_list] + + # --- 订单需求标签 --- + xuqiu_tags_map = {} + if order_ids: + try: + dbq_qs = DingdanBiaoqian.objects.filter( + dingdan_id__in=order_ids + ).select_related('chenghao') + for dbq in dbq_qs: + oid = dbq.dingdan_id + if oid not in xuqiu_tags_map: + xuqiu_tags_map[oid] = [] + ch = dbq.chenghao + xuqiu_tags_map[oid].append({ + 'id': ch.id, + 'mingcheng': ch.mingcheng, + 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu) + }) + except Exception as e: + logger.error(f"查询订单需求标签失败: {e}", exc_info=True) + + # --- 所有需要查询标签的用户ID集合(指定打手 + 商家) --- + yonghuid_set = set(zhiding_ids) + shangjia_order_ids = [item['dingdan_id'] for item in dingdan_list if item['fadan_pingtai'] == 2] + shangjia_id_map = {} + if shangjia_order_ids: + # 使用跨表查询 dingdan__dingdan_id__in 匹配字符串订单ID + sj_exts = DingdanShangjia.objects.filter( + dingdan__dingdan_id__in=shangjia_order_ids + ).values('dingdan_id', 'shangjia_id') + for sj in sj_exts: + shangjia_id_map[sj['dingdan_id']] = sj['shangjia_id'] + if sj['shangjia_id']: + yonghuid_set.add(sj['shangjia_id']) + + # 将 yonghuid 字符串转换为 UserMain 的主键 id + user_id_map = {} + if yonghuid_set: + users = UserMain.objects.filter(yonghuid__in=yonghuid_set).values('id', 'yonghuid') + for u in users: + user_id_map[u['yonghuid']] = u['id'] + + user_main_ids = list(user_id_map.values()) + user_tags_map = {} + if user_main_ids: + uc_qs = YonghuChenghao.objects.filter( + yonghu_id__in=user_main_ids + ).select_related('chenghao') + for uc in uc_qs: + uid = uc.yonghu_id + if uid not in user_tags_map: + user_tags_map[uid] = [] + user_tags_map[uid].append(uc.chenghao) + + dashou_tags_map = {} + shangjia_tags_map = {} + for item in dingdan_list: + oid = item['dingdan_id'] + # 指定打手标签 + zhid = item['zhiding_id'] + dashou_tags = [] + if zhid and zhid in user_id_map: + uid = user_id_map[zhid] + if uid in user_tags_map: + for ch in user_tags_map[uid]: + if ch.leixing == 'dashou': + dashou_tags.append({ + 'id': ch.id, + 'mingcheng': ch.mingcheng, + 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu) + }) + dashou_tags_map[oid] = dashou_tags + + # 商家标签 + sj_tags = [] + if item['fadan_pingtai'] == 2 and oid in shangjia_id_map: + sj_yonghuid = shangjia_id_map[oid] + if sj_yonghuid and sj_yonghuid in user_id_map: + uid = user_id_map[sj_yonghuid] + if uid in user_tags_map: + for ch in user_tags_map[uid]: + if ch.leixing == 'shangjia': + sj_tags.append({ + 'id': ch.id, + 'mingcheng': ch.mingcheng, + 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu) + }) + shangjia_tags_map[oid] = sj_tags + + # 12. 格式化返回数据 + formatted_list = [] + for order in dingdan_list: + oid = order['dingdan_id'] + zid = order.get('zhiding_id') + info = zhiding_info.get(zid, {}) + formatted_list.append({ + 'dingdan_id': order['dingdan_id'], + 'zhuangtai': order['zhuangtai'], + 'pingtai': order['fadan_pingtai'], + 'fadan_pingtai': order['fadan_pingtai'], + 'dashou_fencheng': float(order['dashou_fencheng']) if order['dashou_fencheng'] else 0.0, + 'zhiding_id': order['zhiding_id'] or '', + 'leixing_id': order['leixing_id'] or 0, + 'tupian': order['tupian'] or '', + 'yaoqiuleixing': order['yaoqiuleixing'] or 0, + 'huiyuan_id': order['huiyuan_id'] or '', + 'yajin': float(order['yongjin']) if order['yongjin'] else 0.0, + 'yongjin': float(order['yongjin']) if order['yongjin'] else 0.0, + 'jieshao': order['jieshao'] or '', + 'beizhu': order['beizhu'] or '', + 'creat_time': order['create_time'].strftime('%Y-%m-%d %H:%M:%S') if order['create_time'] else '', + 'sjnicheng': order['sjnicheng'] or '', + 'zhiding_uid': order['zhiding_id'] or '', + 'zhiding_avatar': info.get('avatar', ''), + 'zhiding_nicheng': info.get('nicheng', ''), + # 三层标签 + 'xuqiu_biaoqian': xuqiu_tags_map.get(oid, []), + 'dashou_biaoqian': dashou_tags_map.get(oid, []), + 'shangjia_biaoqian': shangjia_tags_map.get(oid, []), + }) + + has_more = (page * page_size) < total + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'list': formatted_list, + 'has_more': has_more, + 'total': total, + 'current_page': page, + 'page_size': page_size, + 'total_pages': (total + page_size - 1) // page_size, + } + }) + + except Exception as e: + logger.error(f"订单列表查询失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + def _parse_texiao_json(self, texiao_miaoshu): + if not texiao_miaoshu: + return {} + try: + return json.loads(texiao_miaoshu) + except: + return {} + + +# apps/dingdan/views.py +from shangpin.models import ShangpinLeixing # 假设这是你的商品类型模型 + + +class ShangpinLeixingHuoquView(APIView): + """ + 获取商品类型列表 + POST /dingdan/ddlxhq + 需要JWT Token认证 + """ + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 查询商品类型表 + # 假设你的商品类型模型字段为:id, jieshao, tupian_url + leixing_list = ShangpinLeixing.objects.all().values( + 'id', # 商品类型ID + 'jieshao', # 类型介绍 + 'tupian_url' # 类型图片URL(相对URL) + ).order_by('id') # 按ID排序,可根据需要调整 + + # 2. 格式化返回数据 + formatted_list = [] + for leixing in leixing_list: + formatted_leixing = { + 'id': leixing['id'], + 'jieshao': leixing['jieshao'] or '', # 处理空值 + 'tupian_url': leixing['tupian_url'] or '' # 处理空值 + } + formatted_list.append(formatted_leixing) + + # 3. 返回响应 + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'list': formatted_list, + 'total': len(formatted_list) + } + }) + + +# 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') + + +# ----------------- 基础配置获取 ----------------- +def get_self_goeasy_appkey(): + try: + cfg = ClubConfig.objects.filter(is_self=1).first() + return cfg.chat_api_key if cfg else None + except Exception: + return None + + +def get_self_goeasy_secret(): + try: + cfg = ClubConfig.objects.filter(is_self=1).first() + return cfg.chat_api_id if cfg else None + except Exception: + return None + + +def get_self_storage_domain(): + try: + cfg = ClubConfig.objects.filter(is_self=1).first() + return cfg.storage_bucket_domain if cfg else '' + except Exception: + return '' + + +def full_avatar(relative_url, bucket_domain=None): + if not relative_url: + return '' + if relative_url.startswith('http'): + return relative_url + domain = bucket_domain or get_self_storage_domain() + if domain: + domain = domain.rstrip('/') + '/' + return domain + relative_url.lstrip('/') + return relative_url + + +# ----------------- GoEasy API 封装 ----------------- +def subscribe_users_to_group(user_ids, group_ids, appkey=None, secret=None): + """订阅用户到群组,确保能收发该群消息""" + if not appkey: + appkey = get_self_goeasy_appkey() + if not secret: + secret = get_self_goeasy_secret() + if not appkey: + logger.error("订阅失败:GoEasy AppKey 未配置") + return False + url = 'https://rest-hangzhou.goeasy.io/v2/im/subscribe-groups' + body = {"appkey": appkey, "userIds": user_ids, "groupIds": group_ids} + headers = {"Content-Type": "application/json"} + if secret: + headers["Authorization"] = f"Bearer {secret}" + try: + r = requests.post(url, headers=headers, json=body, timeout=10) + if r.status_code == 200: + logger.info(f"订阅成功:用户 {user_ids} -> 群 {group_ids}") + return True + logger.error(f"订阅失败:{r.status_code} {r.text}") + return False + except Exception as e: + logger.exception("订阅异常") + return False + + +def send_group_message( + appkey, secret, group_id, sender_id, sender_name, sender_avatar, + message_text='', custom_payload=None, group_name='', order_id='', is_cross=0 +): + """ + 通过 GoEasy REST API 发送群消息。 + message_text: 普通文本内容 + custom_payload: 自定义消息体(如订单卡片),此时 type 设为 'order' + """ + url = 'https://rest-hangzhou.goeasy.io/v2/im/message' + to_data = { + "name": group_name or group_id, + "avatar": sender_avatar or '', + "orderId": order_id, + "isCross": is_cross + } + msg_type = 'order' if custom_payload else 'text' + payload = custom_payload if custom_payload else message_text + + body = { + "appkey": appkey, + "senderId": sender_id, + "senderData": {"avatar": sender_avatar, "name": sender_name}, + "to": {"type": "group", "id": group_id, "data": to_data}, + "type": msg_type, + "payload": payload + } + headers = {"Content-Type": "application/json"} + if secret: + headers["Authorization"] = f"Bearer {secret}" + + for attempt in range(3): # 最多重试3次 + try: + r = requests.post(url, headers=headers, json=body, timeout=10) + if r.status_code == 200: + logger.info(f"消息发送成功:sender={sender_id}, group={group_id}, type={msg_type}") + return True + logger.warning(f"消息发送失败 (尝试 {attempt+1}/3):状态码 {r.status_code}, 响应 {r.text}") + except Exception as e: + logger.warning(f"消息发送异常 (尝试 {attempt+1}/3):{e}") + if attempt < 2: + time.sleep(1) + logger.error(f"消息最终发送失败:sender={sender_id}, group={group_id}") + return False + + +# ----------------- 权限校验 ----------------- +def check_user_permission(identity_type, uid, order): + """验证用户身份、账号状态、以及是否属于该订单""" + try: + user = UserMain.objects.get(yonghuid=uid) + except UserMain.DoesNotExist: + return False, '用户不存在' + + if identity_type == 'dashou': + try: + dashou = UserDashou.objects.get(user=user) + if dashou.zhanghaozhuangtai != 1 : + return False, '打手账号已被封禁或状态异常' + except UserDashou.DoesNotExist: + return False, '该用户不是打手' + if order.jiedan_dashou_id != uid: + return False, '你不是该订单的接单打手' + + elif identity_type == 'shangjia': + try: + shop = UserShangjia.objects.get(user=user) + if shop.zhuangtai != 1: + return False, '商家账号已被禁用' + except UserShangjia.DoesNotExist: + return False, '该用户不是商家' + try: + if order.shangjia_kuozhan.shangjia_id != uid: + return False, '你不是该订单的发布商家' + except DingdanShangjia.DoesNotExist: + return False, '订单缺少商家信息' + + elif identity_type == 'boss': + try: + UserBoss.objects.get(user=user) # 老板暂无封禁字段,仅需存在 + except UserBoss.DoesNotExist: + return False, '该用户不是老板' + try: + if order.pingtai_kuozhan.laoban_id != uid: + return False, '你不是该订单的老板' + except DingdanPingtai.DoesNotExist: + return False, '订单缺少平台信息' + + else: + return False, '不支持的身份类型' + return True, '' + + +# ----------------- 跨平台对方配置获取 ----------------- +def get_partner_club_config(partner_club_id): + try: + return ClubConfig.objects.get(club_id=partner_club_id, is_self=0) + except ClubConfig.DoesNotExist: + return None + + +# ----------------- 主视图 ----------------- + + +class DaiLiQunLiaoXiaoXiView(APIView): + """ + 代理发送群聊消息 + 支持: + - 非跨平台订单:直接用我方 GoEasy Key 以真实身份发送 + - 跨平台订单(双方任意一方派单):除了我方群外,还需用对方 GoEasy Key 以代理身份向对方群发送 + 跨平台判断条件:订单同时存在 partner_order_id 和 partner_club_id 即视为跨平台 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + # ===== 1. 参数解析 ===== + order_id = request.data.get('orderId') + identity_type = request.data.get('identityType') # dashou / shangjia / boss + sender_name = request.data.get('senderName', '') + sender_avatar_raw = request.data.get('senderAvatar', '') + message_type = request.data.get('messageType', 'text') + message_text = request.data.get('text', '') + order_payload = request.data.get('orderPayload', None) + + logger.info(f"[群聊代理] 收到请求: orderId={order_id}, identityType={identity_type}, " + f"messageType={message_type}, text={message_text[:20]}...") + + if not order_id or not identity_type: + return Response({'code': 400, 'msg': '缺少 orderId 或 identityType'}) + if identity_type not in ('dashou', 'shangjia', 'boss'): + return Response({'code': 400, 'msg': 'identityType 无效'}) + + # ===== 2. 构造 senderId ===== + uid = request.user.yonghuid + prefix_map = {'dashou': 'Ds', 'shangjia': 'Sj', 'boss': 'Boss'} + sender_id = prefix_map[identity_type] + uid + logger.info(f"[群聊代理] 登录用户 yonghuid={uid}, 构造 senderId={sender_id}") + + # ===== 3. 查询订单 ===== + try: + order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=order_id) + except Dingdan.DoesNotExist: + logger.warning(f"[群聊代理] 订单 {order_id} 不存在") + return Response({'code': 404, 'msg': '订单不存在'}) + + # ===== 4. 权限校验 ===== + valid, err_msg = check_user_permission(identity_type, uid, order) + if not valid: + logger.warning(f"[群聊代理] 权限校验失败: {err_msg}, uid={uid}, identity={identity_type}") + return Response({'code': 403, 'msg': err_msg}) + + # ===== 5. 我方 GoEasy 配置 ===== + self_appkey = get_self_goeasy_appkey() + self_secret = get_self_goeasy_secret() + if not self_appkey: + logger.error("[群聊代理] 我方 GoEasy AppKey 未配置") + return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) + + # ===== 6. 公共群信息 ===== + real_group_id = f"group_{order.dingdan_id}" + group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else f"订单{order.dingdan_id}" + sender_avatar = full_avatar(sender_avatar_raw) if sender_avatar_raw else full_avatar('') + + # ===== 7. 订阅发送者到群 ===== + if not subscribe_users_to_group([sender_id], [real_group_id], self_appkey, self_secret): + logger.error("[群聊代理] 订阅群组失败") + return Response({'code': 500, 'msg': '订阅群组失败'}) + + # ===== 8. 判断跨平台(只看对方订单ID和俱乐部ID) ===== + partner_order_id = order.partner_order_id + partner_club_id = order.partner_club_id + is_cross = 1 if (partner_order_id and partner_club_id) else 0 + logger.info(f"[群聊代理] 订单 {order_id} partner_order_id={partner_order_id}, partner_club_id={partner_club_id}, 判定跨平台={is_cross}") + + custom_payload = order_payload if message_type == 'order' else None + text_content = message_text + + # ---------- 先向我方群发送消息(永远执行) ---------- + local_ok = send_group_message( + appkey=self_appkey, + secret=self_secret, + group_id=real_group_id, + sender_id=sender_id, + sender_name=sender_name, + sender_avatar=sender_avatar, + message_text=text_content, + custom_payload=custom_payload, + group_name=group_name, + order_id=order.dingdan_id, + is_cross=is_cross + ) + if not local_ok: + return Response({'code': 500, 'msg': '消息发送失败'}) + + # ---------- 跨平台转发(如果有对方订单ID和俱乐部ID) ---------- + if is_cross: + logger.info("[群聊代理] 满足跨平台条件,准备双平台发送") + partner_cfg = get_partner_club_config(partner_club_id) + if not partner_cfg or not partner_cfg.chat_api_key: + logger.warning("[群聊代理] 对方俱乐部配置不可用,仅发送我方群") + return Response({'code': 200, 'msg': '发送成功(对方GoEasy未配置)'}) + + partner_appkey = partner_cfg.chat_api_key + partner_secret = partner_cfg.chat_api_id or '' + partner_group_id = f"group_{partner_order_id}" + + # 构造代理身份 + agent_sender_id = f"{order.dingdan_id}_{sender_id}" + if identity_type == 'dashou': + agent_sender_name = f"[我方]{sender_name}" + elif identity_type == 'shangjia': + agent_sender_name = f"[我方商家]{sender_name}" + else: + agent_sender_name = f"[我方老板]{sender_name}" + agent_avatar = full_avatar('') + + logger.info(f"[群聊代理] 对方群组={partner_group_id}, 代理身份={agent_sender_id}/{agent_sender_name}") + + # 向对方群发送代理消息 + partner_ok = send_group_message( + appkey=partner_appkey, + secret=partner_secret, + group_id=partner_group_id, + sender_id=agent_sender_id, + sender_name=agent_sender_name, + sender_avatar=agent_avatar, + message_text=text_content, + custom_payload=custom_payload, + group_name='跨平台订单交流', + order_id=partner_order_id, + is_cross=1 + ) + logger.info(f"[群聊代理] 对方群发送结果: {partner_ok}") + + if partner_ok: + return Response({'code': 200, 'msg': '发送成功(双平台)'}) + else: + return Response({'code': 200, 'msg': '我方发送成功,对方发送失败'}) + else: + logger.info("[群聊代理] 非跨平台,仅发送我方群") + return Response({'code': 200, 'msg': '发送成功'}) + + +#通过跨平台字段判断是否是跨平台 + + +from peizhi.models import ClubConfig, ShangjiaLianjie +logger = logging.getLogger('dingdan') + +# ---------- 基础配置 ---------- +def get_self_goeasy_appkey(): + try: + cfg = ClubConfig.objects.filter(is_self=1).first() + return cfg.chat_api_key if cfg else None + except Exception: + return None + +def get_self_goeasy_secret(): + try: + cfg = ClubConfig.objects.filter(is_self=1).first() + return cfg.chat_api_id if cfg else None + except Exception: + return None + +def get_partner_club_config(partner_club_id): + try: + return ClubConfig.objects.get(club_id=partner_club_id, is_self=0) + except ClubConfig.DoesNotExist: + return None + + +class ShangjiaLianjieLiuYanZhuanFaView(APIView): + """ + 商家链接留言转发接口 + 路径:POST /dingdan/sjljlbxxzf + 固定以 B{商家ID} 身份发送群消息。通过 partner_order_id/partner_club_id 判断是否跨平台。 + """ + permission_classes = [] + + def post(self, request): + token = request.data.get('token') + text = request.data.get('text', '') + + if not token or not text: + return Response({'code': 400, 'msg': '缺少参数'}) + + try: + lianjie = ShangjiaLianjie.objects.get(lianjie_token=token) + except ShangjiaLianjie.DoesNotExist: + return Response({'code': 404, 'msg': '链接不存在或已失效'}) + + if not lianjie.dingdan_id: + return Response({'code': 400, 'msg': '链接缺少订单ID'}) + + try: + order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=lianjie.dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 500, 'msg': '订单数据异常'}) + + shangjia_uid = lianjie.yonghu_id + sender_id = f"B{shangjia_uid}" + sender_name = "老板" + sender_avatar = '' + + appkey = get_self_goeasy_appkey() + secret = get_self_goeasy_secret() + if not appkey: + return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) + + group_id = f"group_{order.dingdan_id}" + group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else f"订单{order.dingdan_id}" + + local_cross_flag = 1 if (order.partner_order_id and order.partner_club_id) else 0 + + if not _subscribe_users_to_group([sender_id], [group_id], appkey, secret): + return Response({'code': 500, 'msg': '订阅群组失败'}) + + local_ok = _send_group_message( + appkey=appkey, + secret=secret, + group_id=group_id, + sender_id=sender_id, + sender_name=sender_name, + sender_avatar=sender_avatar, + message_text=text, + custom_payload=None, + group_name=group_name, + order_id=order.dingdan_id, + is_cross=local_cross_flag + ) + if not local_ok: + return Response({'code': 500, 'msg': '消息发送失败'}) + + # 跨平台转发 + partner_order_id = order.partner_order_id + partner_club_id = order.partner_club_id + + if not partner_order_id or not partner_club_id: + return Response({'code': 200, 'msg': '发送成功'}) + + partner_cfg = get_partner_club_config(partner_club_id) + if not partner_cfg or not partner_cfg.chat_api_key: + return Response({'code': 200, 'msg': '发送成功(对方未配置)'}) + + agent_sender_id = f"{order.dingdan_id}_{sender_id}" + agent_sender_name = f"[我方老板]{shangjia_uid}" + + partner_group_id = f"group_{partner_order_id}" + + partner_ok = _send_group_message( + appkey=partner_cfg.chat_api_key, + secret=partner_cfg.chat_api_id or '', + group_id=partner_group_id, + sender_id=agent_sender_id, + sender_name=agent_sender_name, + sender_avatar='', + message_text=text, + custom_payload=None, + group_name='跨平台订单交流', + order_id=partner_order_id, + is_cross=1 + ) + + if partner_ok: + return Response({'code': 200, 'msg': '发送成功(双平台)'}) + else: + 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] + + def post(self, request): + data = request.data + dingdan_id = data.get('dingdan_id') + if not dingdan_id: + return Response({'code': 400, 'msg': '订单ID不能为空'}, status=400) + + try: + with transaction.atomic(): + # 1. 验证打手身份和状态 + dashou_profile = self._validate_dashou_status(request.user) + if not dashou_profile: + return Response({'code': 403, 'msg': '您不是打手或账号状态异常'}, status=403) + + + # ==================== 【新增】罚单检查 ==================== + 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}) + # =========================================================== + + # 2. 锁定并验证订单(行锁) + order_result = self._validate_and_lock_order(dingdan_id, request.user.yonghuid) + if isinstance(order_result, str): + return Response({'code': 400, 'msg': order_result}) + order = order_result + + is_cross = order.is_cross == 1 + dispatch_type = order.dispatch_type + + if is_cross and dispatch_type == 2: + validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) + if not validation_result['success']: + return Response({'code': 400, 'msg': validation_result['message']}) + + claim_allowed = self._request_partner_check_claim(order, request.user.yonghuid) + if claim_allowed is None: + return Response({'code': 500, 'msg': '跨平台通信失败,请重试'}, status=500) + elif claim_allowed is False: + if order.zhuangtai == 1: + order.zhuangtai = 21 + order.save() + logger.info(f"订单 {dingdan_id} 被对方拒绝抢单,状态改为21") + else: + order.zhuangtai = 20 + order.save() + logger.info(f"订单 {dingdan_id} 已被其他平台抢走,状态改为20") + return Response({'code': 400, 'msg': '订单已被其他平台打手抢走或不可抢'}) + + self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) + + try: + update_daily_dispatch_stat( + direction=2, + partner_club_id=order.partner_club_id or '000001', + claimed_amount=order.jine, + claimed_count=1 + ) + except Exception as e: + logger.error(f"更新抢单统计失败: {e}", exc_info=True) + + elif is_cross and dispatch_type == 1: + validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) + if not validation_result['success']: + return Response({'code': 400, 'msg': validation_result['message']}) + + self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) + self._async_notify_partners_order_claimed(order) + + else: + validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid) + if not validation_result['success']: + return Response({'code': 400, 'msg': validation_result['message']}) + self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) + + # ========== 【唯一改动】聊天建立方式 ========== + chat_success = establish_order_chat(dingdan_id) + + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=1 + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + + + # 在 QiangdanView.post 里,_execute_qiangdan 之后添加: + from dengji.services import record_dashou_biaoxian + # 记录抢单表现(不影响主流程) + # 如果订单表里有 bankuai 信息,可以用 order.bankuai 代替 None + record_dashou_biaoxian( + dingdan_id=dingdan_id, + dashou_id=order.jiedan_dashou_id, + xingwei=1, + jiedan_time=timezone.now(), # 直接取当前时间 + bankuai=None # 暂不填,后续提交时补上 + ) + + return Response({ + 'code': 200, + 'msg': '抢单成功', + 'data': { + 'dingdan_id': dingdan_id, + 'chat_established': chat_success + } + }) + + except Exception as e: + logger.error(f"抢单异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '系统异常,请稍后重试'}, status=500) + + # ---------- 以下所有方法原封不动照抄旧版 ---------- + def _validate_dashou_status(self, user): + try: + dashou_profile = user.dashou_profile + if (dashou_profile.zhuangtai != 1 or + dashou_profile.zhanghaozhuangtai != 1 or + dashou_profile.jifen < 5): + return None + return dashou_profile + except UserDashou.DoesNotExist: + return None + + def _validate_and_lock_order(self, dingdan_id, dashou_yonghuid): + try: + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + if order.zhuangtai not in [1, 7]: + return "订单状态不可抢" + if order.zhuangtai == 7: + if not order.zhiding_id or order.zhiding_id != dashou_yonghuid: + return "此订单为指定单,您不是指定打手" + if order.jiedan_dashou_id: + return "订单已被其他打手抢走" + return order + except Dingdan.DoesNotExist: + return "订单不存在" + + def _validate_order_requirements(self, order, dashou_profile, dashou_yonghuid): + if order.yaoqiuleixing == 1: + return self._validate_huiyuan_requirement(order, dashou_yonghuid) + elif order.yaoqiuleixing == 2: + return self._validate_yajin_requirement(order, dashou_profile) + return {'success': True, 'message': '验证通过'} + + def _validate_huiyuan_requirement(self, order, dashou_yonghuid): + try: + huiyuan_record = Huiyuangoumai.objects.get( + huiyuan_id=order.huiyuan_id, + yonghu_id=dashou_yonghuid + ) + if huiyuan_record.jiance_shifou_daoqi() or huiyuan_record.huiyuan_zhuangtai != 1: + return {'success': False, 'message': '会员已过期或未开通对应会员'} + return {'success': True, 'message': '会员验证通过'} + except Huiyuangoumai.DoesNotExist: + return {'success': False, 'message': '未开通对应会员,无法抢此订单'} + + def _validate_yajin_requirement(self, order, dashou_profile): + yajin_required = order.yongjin or 0 + dashou_yajin = dashou_profile.yajin or 0 + if dashou_yajin < yajin_required: + return {'success': False, 'message': f'押金不足,需要{yajin_required}元,当前有{dashou_yajin}元'} + return {'success': True, 'message': '押金验证通过'} + + def _execute_qiangdan(self, order, dashou_profile, dashou_yonghuid): + order.zhuangtai = 2 + order.jiedan_dashou_id = dashou_yonghuid + order.user2_id = f"Ds{dashou_yonghuid}" + order.save() + + dashou_profile.zhuangtai = 0 + dashou_profile.jiedanzongliang = F('jiedanzongliang') + 1 + dashou_profile.jinrijiedan = F('jinrijiedan') + 1 + dashou_profile.jinyuejiedan = F('jinyuejiedan') + 1 + dashou_profile.save() + + # ---------- 跨平台询问对方(照抄) ---------- + def _request_partner_check_claim(self, order, dashou_id): + partner_club_id = order.partner_club_id or '000001' + try: + club = Club.objects.filter(partner_club_id=partner_club_id).first() + if not club or not club.partner_domain: + logger.error(f"未找到对方俱乐部配置: partner_club_id={partner_club_id}") + return None + + my_club = Club.objects.filter(partner_club_id=club.partner_club_id).first() + if not my_club: + logger.error(f"未找到我方俱乐部配置: partner_club_id={club.partner_club_id}") + return None + + payload = { + 'token': settings.CROSS_PLATFORM_TOKEN, + 'wofangdingdan_id': order.partner_order_id, + 'partner_order_id': order.dingdan_id, + 'our_club_id': my_club.club_id, + 'partner_club_id': club.partner_club_id, + 'dashou_id': dashou_id, + 'sj_id': f"Sj{settings.AGENT_SHANGJIA_ID}" + } + + url = club.partner_domain.rstrip('/') + '/dingdan/partner_check_claim' + resp = requests.post(url, json=payload, timeout=5) + + if resp.status_code != 200: + logger.error(f"跨平台抢单询问HTTP失败: {resp.status_code}") + return None + + data = resp.json() + if data.get('code') == 0: + return data.get('data', {}).get('can_claim') is True + else: + logger.error(f"跨平台抢单询问业务失败: {data}") + return False + except Exception as e: + logger.error(f"跨平台抢单询问异常: {e}", exc_info=True) + return None + + # ---------- 异步通知其他俱乐部 ---------- + def _async_notify_partners_order_claimed(self, order): + def notify_task(): + clubs = Club.objects.filter(is_interop_enabled=True).select_related() + for club in clubs: + for attempt in range(1, 4): + success = self._send_claim_notification_to_partner(order, club) + if success: + break + if attempt < 3: + time.sleep(2 ** attempt) + else: + logger.error(f"通知俱乐部 {club.club_id} 订单已抢失败,重试3次后仍失败") + + thread = threading.Thread(target=notify_task) + thread.daemon = True + thread.start() + + def _send_claim_notification_to_partner(self, order, club): + try: + my_club = Club.objects.filter(partner_club_id=club.partner_club_id).first() + if not my_club: + logger.warning(f"未找到我方俱乐部配置: partner_club_id={club.partner_club_id}") + return False + + payload = { + 'token': settings.CROSS_PLATFORM_TOKEN, + 'our_club_id': my_club.club_id, + 'partner_club_id': club.partner_club_id, + 'partner_order_id': order.dingdan_id, + 'claimed': True + } + url = club.partner_domain.rstrip('/') + '/dingdan/partner_notify_claim' + resp = requests.post(url, json=payload, timeout=3) + return resp.status_code == 200 + except Exception as e: + logger.error(f"通知俱乐部 {club.club_id} 异常: {e}") + return False + + +class DashouDingdanHuoquView1(APIView): + """ + 打手订单获取接口 + 支持筛选条件: + - zhuangtai_list: 订单状态列表(可选,空数组或不传表示全部状态) + - leixing_id: 商品类型ID(可选) + - keyword: 搜索关键词,模糊匹配订单ID和订单介绍(可选) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取当前登录用户的 yonghuid + try: + current_user = request.user + yonghuid = current_user.yonghuid + except AttributeError: + return Response({ + 'code': 1, + 'msg': '用户认证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 获取并校验前端参数 + try: + data = request.data + zhuangtai_list = data.get('zhuangtai_list', []) # 可选,默认空列表(全部) + page = max(1, int(data.get('page', 1))) + page_size = min(100, max(1, int(data.get('page_size', 10)))) + leixing_id = data.get('leixing_id', None) # 可选,商品类型ID + keyword = data.get('keyword', '').strip() # 可选,搜索关键词 + except (ValueError, TypeError): + return Response({ + 'code': 2, + 'msg': '参数格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 参数默认值与类型修正 + if not isinstance(zhuangtai_list, list): + zhuangtai_list = [] # 非列表则视为全部 + if leixing_id is not None: + try: + leixing_id = int(leixing_id) + except (ValueError, TypeError): + leixing_id = None + + # 4. 构建查询条件 + try: + # 基础条件:当前打手接单 + query = Q(jiedan_dashou_id=yonghuid) + + # 状态筛选:如果 zhuangtai_list 不为空列表,则加入筛选;否则查询全部状态 + if zhuangtai_list: + query &= Q(zhuangtai__in=zhuangtai_list) + + # 商品类型筛选 + if leixing_id is not None: + query &= Q(leixing_id=leixing_id) + + # 关键词搜索:模糊匹配订单ID和订单介绍 + if keyword: + keyword_query = Q(dingdan_id__icontains=keyword) | Q(jieshao__icontains=keyword) + query &= keyword_query + + # 5. 执行查询,按创建时间倒序 + dingdan_query = Dingdan.objects.filter(query).order_by('-create_time') + + # 只选择需要的字段,提高性能 + dingdan_query = dingdan_query.only( + 'dingdan_id', + 'zhuangtai', + 'dashou_fencheng', # 打手分成金额 + 'jieshao', + 'tupian', + 'nicheng', + 'create_time' + ) + + total_count = dingdan_query.count() + + # 6. 分页处理 + start_index = (page - 1) * page_size + end_index = start_index + page_size + current_page_orders = dingdan_query[start_index:end_index] + current_page_count = len(current_page_orders) + + # 7. 构建返回数据列表 + order_list = [] + for order in current_page_orders: + order_data = { + 'dingdan_id': order.dingdan_id, + 'zhuangtai': order.zhuangtai, + 'jine': float(order.dashou_fencheng) if order.dashou_fencheng else 0.0, + 'jieshao': order.jieshao or '', + 'tupian': order.tupian or '', + 'nicheng': order.nicheng or '', + 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '' + } + order_list.append(order_data) + + # 8. 判断是否还有更多数据 + has_more = (current_page_count == page_size) and (end_index < total_count) + if total_count == 0: + has_more = False + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': { + 'list': order_list, + 'has_more': has_more, + 'total': total_count + } + }, 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({ + 'code': 4, + 'msg': '服务器内部错误', + 'data': None + }, 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上传凭证接口(调试版本)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 接收参数(修复 int 无 strip 问题) + dingdan_id = str(request.data.get('dingdan_id', '')).strip() + yongtu = request.data.get('yongtu', 'dingdan') + + # 2. 条件判断修正:除了 fakuan 之外,其他用途均需提供 dingdan_id + if yongtu != 'fakuan' and not dingdan_id: + return Response( + {'code': 400, 'msg': '订单ID不能为空', 'error': 'dingdan_id is required when yongtu is not fakuan'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 3. 业务验证逻辑(根据用途分别验证) + if yongtu == 'chufa': + try: + from .models import Chufajilu + chufa_record = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + dashouid=request.user.yonghuid, + sqzhuangtai__in=[0, 3] + ).first() + if not chufa_record: + return Response( + {'code': 403, 'msg': '处罚记录无权限或状态不对', + 'error': f'No punishment record found for user {request.user.yonghuid} on order {dingdan_id}'}, + status=status.HTTP_403_FORBIDDEN + ) + except Exception as e: + logger.error(f"查处罚记录异常: {str(e)}") + return Response( + {'code': 500, 'msg': '服务器内部错误', 'error': str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + elif yongtu == 'fakuan': + # 罚款申诉不需要订单验证 + pass + else: + # 默认为订单相关,需要验证订单且打手必须接单 + try: + from .models import Dingdan + dingdan = Dingdan.objects.get( + dingdan_id=dingdan_id, + jiedan_dashou_id=request.user.yonghuid, + zhuangtai__in=[2, 8] + ) + except Dingdan.DoesNotExist: + return Response( + {'code': 403, 'msg': '此功能暂未开放', 'error': f'Order {dingdan_id} not found or not assigned to user {request.user.yonghuid}'}, + status=status.HTTP_403_FORBIDDEN + ) + + # 4. 获取腾讯云STS临时密钥 + try: + # 初始化固定凭证 + cred = credential.Credential( + settings.COS_SECRET_ID, + settings.COS_SECRET_KEY + ) + + # 创建STS客户端 + http_profile = HttpProfile() + http_profile.endpoint = "sts.tencentcloudapi.com" + client_profile = ClientProfile() + client_profile.httpProfile = http_profile + client = sts_client.StsClient(cred, "ap-shanghai", client_profile) + + # 构建策略资源 + resources = [] + if yongtu == 'chufa': + resources.append( + f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/dssstp/*" + ) + elif yongtu == 'fakuan': + resources.append( + f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/dashoufakuan/dashoufakuanshensu/*" + ) + else: + resources.append( + f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*" + ) + + policy = { + "version": "2.0", + "statement": [{ + "effect": "allow", + "action": [ + "cos:PutObject", + "cos:PostObject", + "cos:InitiateMultipartUpload", + "cos:UploadPart", + "cos:CompleteMultipartUpload" + ], + "resource": resources + }] + } + + req = models.GetFederationTokenRequest() + req.Name = f"dashou_{dingdan_id}_{int(time.time())}" + req.Policy = json.dumps(policy) + req.DurationSeconds = 7200 + + resp = client.GetFederationToken(req) + credentials = resp.Credentials + + except TencentCloudSDKException as e: + logger.error(f"获取STS临时密钥失败: {str(e)}") + return Response({ + 'code': 500, + 'msg': f'获取上传凭证失败: {str(e)}', + 'error_detail': str(e) + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 5. 构建响应 + current_time = int(time.time()) + upload_dir = 'fakuan/dashoufakuan/dashoufakuanshensu/' if yongtu == 'fakuan' else ( + 'a_long/chfajltp/dssstp/' if yongtu == 'chufa' else f'order/{dingdan_id}/' + ) + response_data = { + 'credentials': { + 'tmpSecretId': credentials.TmpSecretId, + 'tmpSecretKey': credentials.TmpSecretKey, + 'sessionToken': credentials.Token, + }, + 'startTime': current_time, + 'expiredTime': current_time + 7200, + 'bucket': settings.COS_BUCKET, + 'region': settings.COS_REGION, + 'upload_dir': upload_dir, + } + return Response({'code': 0, 'msg': '成功', 'data': response_data}) + + except Exception as e: + # 捕捉所有未预期的异常,返回完整 traceback + logger.error(f'COS凭证接口未知错误: {traceback.format_exc()}') + return Response({ + 'code': 500, + 'msg': f'服务器内部错误: {str(e)}', + 'error_detail': traceback.format_exc() + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class ShangjiaCOSZhengshuView(APIView): + """商家获取COS上传凭证接口(用于处罚申诉图片上传)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 接收参数 + dingdan_id = request.data.get('dingdan_id', '').strip() + yongtu = request.data.get('yongtu', 'chufa') # 用途:chufa=处罚申诉 + + if not dingdan_id: + return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # 2. 🔴【修改】验证商家身份 - 通过反向查询shop_profile判断是否为商家 + try: + # 检查是否有商家扩展表 + shangjia_profile = request.user.shop_profile + + # 验证商家状态是否为1(正常) + if shangjia_profile.zhuangtai != 1: + return Response({'code': 3, 'msg': '商家账号状态异常,无法操作', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + # 🔴【重要】获取商家ID(从商家扩展表中获取) + shangjia_id = request.user.yonghuid # 假设商家扩展表中有yonghuid字段 + + except AttributeError: + # 没有shop_profile属性,说明不是商家 + return Response({'code': 3, 'msg': '非商家用户无权限', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + # 3. 🔴【修改】验证订单权限 - 通过商家订单扩展表验证 + try: + from .models import Dingdan, DingdanShangjia + + # 获取订单 + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + + # 🔴【重要】通过反向查询商家扩展表验证订单是否属于当前商家 + try: + shangjia_kuozhan = dingdan.shangjia_kuozhan # 使用反向关系名 + + # 验证商家ID是否匹配 + if str(shangjia_kuozhan.shangjia_id) != str(shangjia_id): + return Response({'code': 2, 'msg': '订单不属于当前商家', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + # 验证订单状态是否允许申请处罚(根据前端逻辑) + allowed_zhuangtai = [2, 4, 5, 6, 8] + if dingdan.zhuangtai not in allowed_zhuangtai: + return Response({'code': 4, 'msg': '当前订单状态不允许申请处罚', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # 🔴【新增】检查是否已存在处罚申请(通过Chufajilu模型) + # 注意:这里假设Chufajilu模型存在,并且有shangjia_id字段 + try: + from .models import Chufajilu + existing_chufa = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + qingqiuid=shangjia_id, + sqzhuangtai__in=[0, 1] # 审核中或已处罚状态 + ).exists() + + if existing_chufa: + return Response({'code': 5, 'msg': '该订单已有处罚申请,请勿重复上传', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + except ImportError: + # 如果没有Chufajilu模型,跳过这个检查 + pass + + except DingdanShangjia.DoesNotExist: + # 没有商家扩展记录,说明这不是商家订单 + return Response({'code': 2, 'msg': '非商家订单无法操作', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + except Dingdan.DoesNotExist: + return Response({'code': 2, 'msg': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"订单验证异常: {str(e)}") + return Response({'code': 500, 'msg': '服务器验证异常', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 4. 获取腾讯云STS临时密钥 + try: + # 初始化固定凭证 + cred = credential.Credential( + settings.COS_SECRET_ID, + settings.COS_SECRET_KEY + ) + + # 创建STS客户端 + http_profile = HttpProfile() + http_profile.endpoint = "sts.tencentcloudapi.com" + client_profile = ClientProfile() + client_profile.httpProfile = http_profile + + client = sts_client.StsClient(cred, "ap-shanghai", client_profile) + + # 🔴【修改】构建策略 - 只允许上传到商家处罚证据图片目录 + resources = [ + f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/sjzjtp/*" + ] + + policy = { + "version": "2.0", + "statement": [ + { + "effect": "allow", + "action": [ + "cos:PutObject", + "cos:PostObject", + "cos:InitiateMultipartUpload", + "cos:UploadPart", + "cos:CompleteMultipartUpload" + ], + "resource": resources + } + ] + } + + # 请求STS临时凭证 + req = models.GetFederationTokenRequest() + # 🔴【修改】命名包含商家ID + req.Name = f"shangjia_{shangjia_id}_{dingdan_id}_{int(time.time())}" + req.Policy = json.dumps(policy) + req.DurationSeconds = 7200 # 2小时有效期 + + resp = client.GetFederationToken(req) + + # 提取临时密钥 + credentials = resp.Credentials + + except TencentCloudSDKException as e: + logger.error(f"商家获取STS临时密钥失败: {str(e)}") + return Response({ + 'code': 500, + 'msg': f'获取上传凭证失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 5. 构建响应数据 + current_time = int(time.time()) + response_data = { + 'credentials': { + 'tmpSecretId': credentials.TmpSecretId, + 'tmpSecretKey': credentials.TmpSecretKey, + 'sessionToken': credentials.Token, + }, + 'startTime': current_time, + 'expiredTime': current_time + 7200, + 'bucket': settings.COS_BUCKET, + 'region': settings.COS_REGION, + # 🔴【修改】商家专用上传目录 + 'upload_dir': 'a_long/chfajltp/sjzjtp/', + # 🔴【新增】返回商家ID和订单ID,便于前端生成文件名 + 'dingdan_id': dingdan_id, + 'shangjia_id': shangjia_id, + # 🔴【新增】可选:返回商家昵称,便于前端显示 + 'shangjia_nicheng': shangjia_profile.nicheng if hasattr(shangjia_profile, 'nicheng') else '' + } + + # 6. 返回成功 + return Response({ + 'code': 0, + 'msg': '成功', + 'data': response_data + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f'商家COS凭证接口未知错误: {str(e)}', exc_info=True) + return Response({ + 'code': 500, + 'msg': f'服务器内部错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class DashouTijiaoView(APIView): + """打手提交订单完成接口""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取参数 + data = request.data + dingdan_id = data.get('dingdan_id', '') + liuyan = data.get('liuyan', '') + jiaofu_tupian_urls = data.get('jiaofu_tupian_urls', []) + + # 2. 参数验证 + if not dingdan_id: + return Response({ + 'code': 1, + 'msg': '订单ID不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not isinstance(jiaofu_tupian_urls, list): + return Response({ + 'code': 2, + 'msg': '图片URL列表格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 限制图片数量(最多9张,最少1张) + if len(jiaofu_tupian_urls) == 0: + return Response({ + 'code': 3, + 'msg': '至少需要上传一张图片', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if len(jiaofu_tupian_urls) > 9: + return Response({ + 'code': 4, + 'msg': '最多只能上传9张图片', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 留言长度限制(50字) + if liuyan and len(liuyan) > 50: + return Response({ + 'code': 5, + 'msg': '留言最多50个字', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 获取当前用户并验证打手身份 + current_user = request.user + yonghuid = current_user.yonghuid + + try: + dashou_profile = current_user.dashou_profile + except UserDashou.DoesNotExist: + return Response({ + 'code': 6, + 'msg': '用户不是打手身份', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 4. 验证订单状态和权限 + try: + dingdan = Dingdan.objects.get( + dingdan_id=dingdan_id, + jiedan_dashou_id=yonghuid, + zhuangtai=2 # 必须是进行中的订单 + ) + except Dingdan.DoesNotExist: + return Response({ + 'code': 7, + 'msg': '订单不存在或没有提交权限', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 5. 存储图片到Dashoutupian表 + for tupian_url in jiaofu_tupian_urls: + # 创建打手提交图片记录 + Dashoutupian.objects.create( + dingdan_id=dingdan_id, + dashou=yonghuid, + tupian=tupian_url, + create_time=timezone.now() + ) + + # 6. 更新订单主表 + dingdan.dashou_liuyan = liuyan + dingdan.zhuangtai = 8 # 状态改为结算中 + dingdan.update_time = timezone.now() + dingdan.save() + + # 7. 更新打手扩展表状态 + dashou_profile.zhuangtai = 1 # 改为数字1(根据你之前说的) + dashou_profile.save() + + from dengji.services import record_dashou_biaoxian + # ========== 新增:记录打手提交时间 ========== + record_dashou_biaoxian( + dingdan_id=dingdan_id, + dashou_id=yonghuid, + xingwei=2, # 提交 + tijiao_time=timezone.now() + ) + # =========================================== + + # 8. 返回成功响应 + return Response({ + 'code': 0, + 'msg': '订单提交成功', + 'data': { + 'dingdan_id': dingdan_id, + 'new_zhuangtai': 8, # 新的订单状态 + 'dashou_zhuangtai': 1 # 打手新的状态 + } + }, 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({ + 'code': 8, + 'msg': '服务器内部错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class DashouDingdanXiangqingView(APIView): + """打手订单详情获取接口""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdan_id = request.data.get('dingdan_id', '') + if not dingdan_id: + return Response({ + 'code': 1, + 'msg': '订单ID不能为空', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 获取当前用户并验证权限 + current_user = request.user + yonghuid = current_user.yonghuid + + try: + dashou_profile = current_user.dashou_profile + except UserDashou.DoesNotExist: + return Response({ + 'code': 2, + 'msg': '用户不是打手身份', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 3. 查询订单主表 + try: + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({ + 'code': 3, + 'msg': '订单不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 4. 验证订单是否属于当前打手 + if dingdan.jiedan_dashou_id != yonghuid: + return Response({ + 'code': 4, + 'msg': '没有权限查看此订单', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 5. 构建基础订单数据 + dingdan_data = { + 'dingdan_id': dingdan.dingdan_id, + 'zhuangtai': dingdan.zhuangtai, + 'jine': float(dingdan.dashou_fencheng) if dingdan.dashou_fencheng else 0.0, + 'jieshao': dingdan.jieshao or '', + 'tupian': dingdan.tupian or '', + 'nicheng': dingdan.nicheng or '', + 'beizhu': dingdan.beizhu or '', + 'create_time': dingdan.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan.create_time else '', + 'tuikuan_liyou':dingdan.tkly or '', + 'dashou_liuyan': dingdan.dashou_liuyan or '', + 'fuwudashou_id': dingdan.jiedan_dashou_id or '', + 'fadanpingtai': dingdan.fadan_pingtai or '' + } + + # 6. 查询打手提交的图片 + dashou_tupian_list = Dashoutupian.objects.filter( + dingdan_id=dingdan_id, + dashou=yonghuid + ).order_by('create_time') + + jiaofu_tupian_urls = [] + for tupian_record in dashou_tupian_list: + if tupian_record.tupian: + jiaofu_tupian_urls.append(tupian_record.tupian) + + dingdan_data['dashou_jiaofu'] = jiaofu_tupian_urls + + # 7. 查询商家扩展表信息 + shangjia_info = {} + try: + shangjia_kuozhan = dingdan.shangjia_kuozhan + shangjia_info = { + 'shangjia_id': shangjia_kuozhan.shangjia_id or '', + 'shangjia_nicheng': shangjia_kuozhan.sjnicheng or '', + 'shangjia_liuyan': shangjia_kuozhan.shangjia_pingjia or '', + #'tuikuan_liyou': '', + 'chufa_liyou': shangjia_kuozhan.cfliyou or '', + 'chufa_zhuangtai': shangjia_kuozhan.sqzhuangtai or 0, + #'chufa_jieguo': shangjia_kuozhan.chufa_jieguo or '', + 'bohui_liyou': shangjia_kuozhan.bhliyou or '' + } + except DingdanShangjia.DoesNotExist: + # 没有商家扩展信息,返回空值 + shangjia_info = { + 'shangjia_id': '', + 'shangjia_nicheng': '', + 'shangjia_liuyan': '', + 'tuikuan_liyou': '', + 'chufa_liyou': '', + 'chufa_zhuangtai': 0, + 'chufa_jieguo': '', + 'bohui_liyou': '' + } + + # 8. 查询平台扩展表信息(如果有的话) + pingtai_info = {} + try: + pingtai_kuozhan = dingdan.pingtai_kuozhan + pingtai_info = { + 'laoban_id': pingtai_kuozhan.laoban_id or '', + 'laoban_pingjia': pingtai_kuozhan.laoban_pingjia or '' + } + except DingdanPingtai.DoesNotExist: + pingtai_info = { + 'laoban_id': '', + 'laoban_pingjia': '' + } + + # 9. 合并所有数据 + response_data = { + **dingdan_data, + **shangjia_info, + **pingtai_info + } + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': response_data + }, 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({ + 'code': 5, + 'msg': '服务器内部错误', + 'data': None + }, 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): + """ + 后台订单详情获取接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddxq + 参数: {"zhanghao": "管理员手机号", "dingdan_id": "订单ID"} + """ + + 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() + + if not qianzhanghao or not dingdan_id: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. JWT Token身份验证 (IsAuthenticated已处理) + 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. 查询订单主表 + try: + from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia, Dashoutupian + + # 查询订单主表 - 使用select_related预加载关联表 + dingdan_obj = Dingdan.objects.select_related( + 'pingtai_kuozhan', 'shangjia_kuozhan' + ).get(dingdan_id=dingdan_id) + + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 7. 构建返回数据 - 主表字段 + response_data = { + # 基础信息字段 + 'dingdan_id': dingdan_obj.dingdan_id or '', + 'jieshao': dingdan_obj.jieshao or '', + 'zhuangtai': dingdan_obj.zhuangtai or 0, + 'leixing': dingdan_obj.leixing_id or 0, # 前端需要 leixing,对应 leixing_id + 'fadanpingtai': dingdan_obj.fadan_pingtai or 0, # 前端需要 fadanpingtai,对应 fadan_pingtai + 'jiage': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00, # 前端需要 jiage,对应 jine + 'tupian_url': dingdan_obj.tupian or '', # 前端需要 tupian_url,对应 tupian + 'beizhu': dingdan_obj.beizhu or '', + + # 服务信息字段 + 'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '', + 'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00, + 'zhiding_id': dingdan_obj.zhiding_id or '', + 'dashou_liuyan': dingdan_obj.dashou_liuyan 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 '', + } + + # 8. 根据发单平台查询扩展表数据 + fadan_pingtai = dingdan_obj.fadan_pingtai + + if fadan_pingtai == 1: + # 平台发单 - 查询平台订单扩展表 + try: + # 使用反向关联 pingtai_kuozhan + pingtai_kuozhan = dingdan_obj.pingtai_kuozhan + + response_data.update({ + 'laoban_id': pingtai_kuozhan.laoban_id or '', + 'nicheng': dingdan_obj.nicheng or '', # 这个字段在订单主表 + 'laoban_pingjia': pingtai_kuozhan.laoban_pingjia or '', + }) + except DingdanPingtai.DoesNotExist: + # 扩展表不存在,返回空值 + response_data.update({ + 'laoban_id': '', + 'nicheng': dingdan_obj.nicheng or '', + 'laoban_pingjia': '', + }) + + elif fadan_pingtai == 2: + # 商家发单 - 查询商家订单扩展表 + try: + # 使用反向关联 shangjia_kuozhan + shangjia_kuozhan = dingdan_obj.shangjia_kuozhan + + response_data.update({ + 'shangjia_id': shangjia_kuozhan.shangjia_id or '', + 'sjnicheng': shangjia_kuozhan.sjnicheng or '', + 'shangjia_pingjia': shangjia_kuozhan.shangjia_pingjia or '', + 'cfliyou': shangjia_kuozhan.cfliyou or '', # 商家处罚理由 + 'bhliyou': shangjia_kuozhan.bhliyou or '', # 驳回理由 + }) + except DingdanShangjia.DoesNotExist: + # 扩展表不存在,返回空值 + response_data.update({ + 'shangjia_id': '', + 'sjnicheng': '', + 'shangjia_pingjia': '', + 'cfliyou': '', + 'bhliyou': '', + }) + + # 9. 查询打手提交的图片 + try: + # 查询该订单的所有打手提交图片 + dashou_tupian_list = Dashoutupian.objects.filter( + dingdan_id=dingdan_id + ).values_list('tupian', flat=True) + + # 转换为列表,过滤空值 + dashou_images = [img for img in dashou_tupian_list if img] + response_data['dashou_images'] = dashou_images + except Exception as e: + logger.warning(f"查询打手图片失败: {str(e)}") + response_data['dashou_images'] = [] + + # 🔥 新增:查询退款记录表中的商家退款理由 + try: + # 导入退款记录模型 + from dingdan.models import Tuikuanjilu + + # 查询该订单的退款记录(一个订单可能有多次退款申请,我们取最新的) + tuikuan_record = Tuikuanjilu.objects.filter( + dingdan_id=dingdan_id + ).order_by('-create_time').first() # 按创建时间倒序,取最新的 + + if tuikuan_record and tuikuan_record.liyou: + # 如果有退款记录且理由不为空,添加到返回数据 + response_data['tuikuan_liyou'] = tuikuan_record.liyou + else: + # 没有找到退款记录或理由为空 + response_data['tuikuan_liyou'] = '' + except Tuikuanjilu.DoesNotExist: + # 退款记录表不存在该订单的记录 + response_data['tuikuan_liyou'] = '' + except Exception as e: + # 查询过程中出现其他异常 + logger.warning(f"查询退款记录失败: {str(e)}") + response_data['tuikuan_liyou'] = '' + + # 10. 返回成功响应 + 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 + ) + + +# 获取日志器 +logger = logging.getLogger(__name__) + + +class AdGengHuanDaShou(APIView): + """ + 后台更换打手接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddgh + 参数: { + "zhanghao": "管理员账号", + "dingdan_id": "订单ID", + "new_dashou_id": "新打手ID" + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + """ + 更换订单的打手 + 核心逻辑: + 1. 验证管理员身份 + 2. 验证订单存在且状态为进行中(2)或结算中(8) + 3. 验证新打手ID存在且是打手身份 + 4. 验证新打手状态正常(zhuangtai=1且zhanghaozhuangtai=1) + 5. 根据订单的抢单要求类型进行验证: + - 会员抢单(1):验证打手有对应会员且未过期 + - 押金抢单(2):验证打手押金≥订单押金 + 6. 更新新打手状态和统计数据 + 7. 更新原打手状态和统计数据 + 8. 更新订单信息 + """ + try: + # 1. 获取前端参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + new_dashou_id = request.data.get('new_dashou_id', '').strip() + + if not qianzhanghao or not dingdan_id or not new_dashou_id: + 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. 查询订单主表 + try: + from dingdan.models import Dingdan + dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 7. 验证订单状态是否为进行中(2)或结算中(8) + if dingdan_obj.zhuangtai not in [2, 8]: + return Response( + {'code': 400, 'message': '订单状态不允许更换打手', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 8. 验证新打手ID是否存在,并且是打手身份 + try: + new_dashou_user = UserMain.objects.get( + yonghuid=new_dashou_id, + #user_type='dashou' + ) + except UserMain.DoesNotExist: + return Response( + {'code': 404, 'message': '打手不存在或不是打手身份', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 9. 获取新打手扩展表 + try: + new_dashou_profile = new_dashou_user.dashou_profile + except AttributeError: + return Response( + {'code': 404, 'message': '打手信息不完整', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 10. 验证新打手状态:zhuangtai=1且zhanghaozhuangtai=1 + if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1: + return Response( + {'code': 400, 'message': '打手状态不允许接单', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 11. 根据订单的抢单要求类型进行验证 + yaoqiuleixing = dingdan_obj.yaoqiuleixing + + if yaoqiuleixing == 1: # 会员抢单 + # 获取订单的会员ID + huiyuan_id = dingdan_obj.huiyuan_id + if not huiyuan_id: + return Response( + {'code': 400, 'message': '订单会员ID为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 查询会员购买记录 + try: + from shangpin.models import Huiyuangoumai + huiyuan_goumai = Huiyuangoumai.objects.get( + huiyuan_id=huiyuan_id, + yonghu_id=new_dashou_id # 注意:yonghu_id存储的是用户ID(yonghuid) + ) + except Huiyuangoumai.DoesNotExist: + return Response( + {'code': 400, 'message': '打手未购买该订单要求的会员', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 检查会员是否过期 + # ✅ 可以调用!因为jiance_shifou_daoqi()是Huiyuangoumai模型的实例方法 + is_expired = huiyuan_goumai.jiance_shifou_daoqi() + if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1: + return Response( + {'code': 400, 'message': '打手的会员已过期', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + elif yaoqiuleixing == 2: # 押金抢单 + # 获取订单的押金(yongjin字段) + order_yajin = dingdan_obj.yongjin + if order_yajin is None: + return Response( + {'code': 400, 'message': '订单押金为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证打手押金是否大于等于订单押金 + if new_dashou_profile.yajin < order_yajin: + return Response( + {'code': 400, 'message': '打手押金不足', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + else: + # 未知的抢单要求类型 + return Response( + {'code': 400, 'message': '订单抢单要求类型错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 12. 所有验证通过,开始更换打手(使用事务保证一致性) + with transaction.atomic(): + # 12.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() + + # 12.2 如果原订单有接单打手,则更新原打手状态 + old_dashou_id = dingdan_obj.jiedan_dashou_id + if old_dashou_id: + try: + # 查询原打手主表 + old_dashou_user = UserMain.objects.get( + yonghuid=old_dashou_id, + #user_type='dashou' + ) + + # 获取原打手扩展表 + 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: + # 原打手不存在,继续执行不影响更换 + logger.warning(f"原打手{old_dashou_id}不存在,跳过更新") + except AttributeError: + # 原打手扩展表不存在,继续执行不影响更换 + logger.warning(f"原打手{old_dashou_id}扩展信息不存在,跳过更新") + except Exception as e: + # 其他异常,记录日志但继续执行 + logger.error(f"更新原打手状态异常: {str(e)}", exc_info=True) + + # 12.3 更新订单表 + dingdan_obj.jiedan_dashou_id = new_dashou_id + dingdan_obj.zhuangtai = 2 # 进行中状态 + dingdan_obj.save() + + # 13. 返回成功响应 + return Response({ + 'code': 0, + 'message': '更换打手成功', + 'data': None + }) + + 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 logging + +# 获取日志器 +logger = logging.getLogger(__name__) + + +class AdQiangZhiJieDan(APIView): + """ + 后台强制结单接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddqzjd + 参数: { + "zhanghao": "管理员账号", + "dingdan_id": "订单ID" + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + """ + 强制结单接口 + 核心逻辑: + 1. 验证管理员身份 + 2. 验证订单状态为2(进行中)、8(结算中)或4(退款审核中)才能强制结单 + 3. 根据发单平台(1=平台发单, 2=商家发单)分别处理 + 4. 更新订单状态为3(已完成) + 5. 给打手结算分成金额 + 6. 如果是商家发单,还需要更新商家成交订单数量 + """ + try: + # 1. 获取前端参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not qianzhanghao or not dingdan_id: + 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. 查询订单主表 + try: + from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia + dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 7. 验证订单状态是否为2、8或4 + if dingdan_obj.zhuangtai not in [2, 8, 4]: + return Response( + {'code': 400, 'message': '订单状态不允许强制结单', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 8. 获取订单的重要字段 + jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID + dashou_fencheng = dingdan_obj.dashou_fencheng # 打手分成金额 + fadan_pingtai = dingdan_obj.fadan_pingtai # 发单平台 + + # 9. 验证打手分成金额是否存在 + if dashou_fencheng is None: + return Response( + {'code': 400, 'message': '订单缺少打手分成金额', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 10. 使用事务保证数据一致性 + with transaction.atomic(): + # 10.1 更新订单状态为3(已完成) + dingdan_obj.zhuangtai = 3 + dingdan_obj.save() + + # 10.2 处理打手结算 + if jiedan_dashou_id: + try: + # 查询打手用户主表 + dashou_user = UserMain.objects.get( + yonghuid=jiedan_dashou_id, + #user_type='dashou' + ) + + # 获取打手扩展表 + dashou_profile = dashou_user.dashou_profile + + # 更新打手扩展表数据 + # ✅ 确认字段:chengjiaozongliang 成交单总量 + dashou_profile.chengjiaozongliang += 1 + + # ✅ 确认字段:yue 可提现余额 + dashou_profile.yue += dashou_fencheng + + # ✅ 确认字段:zonge 打单赚取总额 + dashou_profile.zonge += dashou_fencheng + + # ✅ 确认字段:jinrishouyi 今日收益金额 + dashou_profile.jinrishouyi += dashou_fencheng + + # ✅ 确认字段:jinyueshouyi 今月收益金额 + dashou_profile.jinyueshouyi += dashou_fencheng + + dashou_profile.save() + logger.info(f"强制结单成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}") + + except UserMain.DoesNotExist: + # 打手用户不存在,记录日志但不影响强制结单 + logger.warning(f"强制结单:打手{jiedan_dashou_id}不存在,跳过打手结算") + except AttributeError: + # 打手扩展表不存在,记录日志但不影响强制结单 + logger.warning(f"强制结单:打手{jiedan_dashou_id}扩展信息不存在,跳过打手结算") + + # 10.3 如果是商家发单,还需要更新商家扩展表 + if fadan_pingtai == 2: + try: + # 获取商家订单扩展表 + shangjia_kuozhan = dingdan_obj.shangjia_kuozhan + shangjia_id = shangjia_kuozhan.shangjia_id + + if shangjia_id: + # 查询商家用户主表 + shangjia_user = UserMain.objects.get( + yonghuid=shangjia_id, + #user_type='shop' + ) + + # 获取商家扩展表 + shangjia_profile = shangjia_user.shop_profile + + # ✅ 确认字段:chengjiao 商家成交订单总量 + shangjia_profile.chengjiao += 1 + shangjia_profile.save() + + logger.info(f"强制结单成功,商家{shangjia_id}成交订单+1") + + except DingdanShangjia.DoesNotExist: + # 商家订单扩展表不存在,跳过商家更新 + logger.warning(f"强制结单:订单{dingdan_id}商家扩展表不存在,跳过商家更新") + except UserMain.DoesNotExist: + # 商家用户不存在,跳过商家更新 + logger.warning(f"强制结单:商家{shangjia_id}不存在,跳过商家更新") + except AttributeError: + # 商家扩展表不存在,跳过商家更新 + logger.warning(f"强制结单:商家{shangjia_id}扩展信息不存在,跳过商家更新") + + # 12. 更新平台收益流水(仅在平台发单时) + if dingdan_obj.fadan_pingtai == 1: + try: + from dingdan.utils import update_platform_profit # 导入函数 + update_platform_profit(dingdan_id) + except Exception as e: + # 平台收益更新失败不影响主流程,但记录日志 + logger.error(f"强制结单后更新平台收益失败: {str(e)}") + + # 11. 返回成功响应 + return Response({ + 'code': 0, + 'message': '强制结单成功', + 'data': None + }) + + 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 AdJuJueTuiKuan(APIView): + """ + 后台拒绝退款接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddjj + 参数: { + "zhanghao": "管理员账号", + "dingdan_id": "订单ID", + "jujue_liyou": "拒绝退款理由(可选)" + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + """ + 拒绝退款接口 + 核心逻辑: + 1. 验证管理员身份 + 2. 拒绝退款后订单状态改为6(退款失败) + 3. 给打手结算分成金额 + 4. 如果是商家发单,更新商家成交订单数量 + 5. 更新退款记录表,状态改为2(驳回) + """ + try: + # 1. 获取前端参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + jujue_liyou = request.data.get('jujue_liyou', '').strip() + + if not qianzhanghao or not dingdan_id: + 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. 查询订单主表 + try: + from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu + dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 7. 获取订单的重要字段 + jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID + dashou_fencheng = dingdan_obj.dashou_fencheng # 打手分成金额 + fadan_pingtai = dingdan_obj.fadan_pingtai # 发单平台 + + # 8. 验证打手分成金额是否存在 + if dashou_fencheng is None: + return Response( + {'code': 400, 'message': '订单缺少打手分成金额', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 9. 使用事务保证数据一致性 + with transaction.atomic(): + # 9.1 更新订单状态为6(退款失败) + dingdan_obj.zhuangtai = 6 + dingdan_obj.save() + + # 9.2 处理打手结算 + if jiedan_dashou_id: + try: + # 查询打手用户主表 + dashou_user = UserMain.objects.get( + yonghuid=jiedan_dashou_id, + #user_type='dashou' + ) + + # 获取打手扩展表 + dashou_profile = dashou_user.dashou_profile + + # 更新打手扩展表数据 + # ✅ 确认字段:chengjiaozongliang 成交单总量 + dashou_profile.chengjiaozongliang += 1 + + # ✅ 确认字段:yue 可提现余额 + dashou_profile.yue += dashou_fencheng + + # ✅ 确认字段:zonge 打单赚取总额 + dashou_profile.zonge += dashou_fencheng + + # ✅ 确认字段:jinrishouyi 今日收益金额 + dashou_profile.jinrishouyi += dashou_fencheng + + # ✅ 确认字段:jinyueshouyi 今月收益金额 + dashou_profile.jinyueshouyi += dashou_fencheng + + dashou_profile.save() + logger.info(f"拒绝退款成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}") + + except UserMain.DoesNotExist: + # 打手用户不存在,记录日志但不影响操作 + logger.warning(f"拒绝退款:打手{jiedan_dashou_id}不存在,跳过打手结算") + except AttributeError: + # 打手扩展表不存在,记录日志但不影响操作 + logger.warning(f"拒绝退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手结算") + + # 9.3 如果是商家发单,还需要更新商家扩展表 + if fadan_pingtai == 2: + try: + # 获取商家订单扩展表 + shangjia_kuozhan = dingdan_obj.shangjia_kuozhan + shangjia_id = shangjia_kuozhan.shangjia_id + + if shangjia_id: + # 查询商家用户主表 + shangjia_user = UserMain.objects.get( + yonghuid=shangjia_id, + #user_type='shop' + ) + + # 获取商家扩展表 + shangjia_profile = shangjia_user.shop_profile + + # ✅ 确认字段:chengjiao 商家成交订单总量 + shangjia_profile.chengjiao += 1 + shangjia_profile.save() + + logger.info(f"拒绝退款成功,商家{shangjia_id}成交订单+1") + + except DingdanShangjia.DoesNotExist: + # 商家订单扩展表不存在,跳过商家更新 + logger.warning(f"拒绝退款:订单{dingdan_id}商家扩展表不存在,跳过商家更新") + except UserMain.DoesNotExist: + # 商家用户不存在,跳过商家更新 + logger.warning(f"拒绝退款:商家{shangjia_id}不存在,跳过商家更新") + except AttributeError: + # 商家扩展表不存在,跳过商家更新 + logger.warning(f"拒绝退款:商家{shangjia_id}扩展信息不存在,跳过商家更新") + + # 9.4 更新退款记录表 + try: + # 查询退款记录表 + tuikuan_jilu = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) + + # ✅ 确认字段:sqzhuangtai 申请状态,改为2(驳回) + tuikuan_jilu.sqzhuangtai = 2 + + # ✅ 确认字段:bhliyou 驳回理由 + tuikuan_jilu.bhliyou = jujue_liyou + + # ✅ 确认字段:chuliid 处理人ID(使用管理员账号,即手机号) + tuikuan_jilu.chuliid = qianzhanghao + + tuikuan_jilu.save() + logger.info(f"拒绝退款成功,更新退款记录表,订单:{dingdan_id}") + + except Tuikuanjilu.DoesNotExist: + # 退款记录表不存在,记录日志但不影响主流程 + logger.warning(f"拒绝退款:订单{dingdan_id}退款记录不存在,跳过更新") + + # 10. 返回成功响应 + return Response({ + 'code': 0, + 'message': '拒绝退款成功', + 'data': None + }) + + 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 AdTongYiTuiKuanShangJia(APIView): + """ + 后台同意退款接口(商家发单) + 权限: JWT Token认证 + 管理员身份验证 + 密码验证 + 前端调用: POST /dingdan/adddsjtk + 参数: { + "zhanghao": "管理员账号", + "dingdan_id": "订单ID", + "tuikuan_liyou": "退款理由(可选)", + "pay_password": "支付密码(openid)" + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + """ + 同意退款接口(商家发单) + 核心逻辑: + 1. 验证管理员身份 + 2. 验证支付密码(openid) + 3. 检查订单状态为4(退款审核中) + 4. 更新订单状态为5(已退款) + 5. 更新打手退款订单数量 + 6. 更新商家退款订单数量和余额(加上订单金额) + 7. 更新退款记录表,状态改为1(成功) + """ + try: + # 1. 获取前端参数 + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() + pay_password = request.data.get('pay_password', '').strip() + + if not qianzhanghao or not dingdan_id or not pay_password: + 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. 验证支付密码(对比openid) + if pay_password != dangqianyonghu.openid: + return Response( + {'code': 401, 'message': '支付密码错误', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 7. 查询订单主表 + try: + from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu + dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 8. 验证订单状态为4(退款审核中) + if dingdan_obj.zhuangtai != 4: + return Response( + {'code': 400, 'message': '订单状态不是退款审核中', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 9. 验证订单为商家发单 + if dingdan_obj.fadan_pingtai != 2: + return Response( + {'code': 400, 'message': '此接口只处理商家发单的退款', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 10. 获取订单的重要字段 + jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID + jine = dingdan_obj.jine # 订单金额(不是打手分成) + + # 11. 验证订单金额是否存在 + if jine is None: + return Response( + {'code': 400, 'message': '订单缺少订单金额', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 12. 获取商家ID + try: + shangjia_kuozhan = dingdan_obj.shangjia_kuozhan + shangjia_id = shangjia_kuozhan.shangjia_id + except DingdanShangjia.DoesNotExist: + return Response( + {'code': 400, 'message': '订单商家信息不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 13. 使用事务保证数据一致性 + with transaction.atomic(): + # 13.1 更新订单状态为5(已退款) + dingdan_obj.zhuangtai = 5 + dingdan_obj.save() + + # 13.2 更新打手扩展表(退款订单总量+1) + if jiedan_dashou_id: + try: + # 查询打手用户主表 + dashou_user = UserMain.objects.get( + yonghuid=jiedan_dashou_id, + #user_type='dashou' + ) + + # 获取打手扩展表 + dashou_profile = dashou_user.dashou_profile + + # ✅ 确认字段:tuikuanliang 退款订单总量 + dashou_profile.tuikuanliang += 1 + + dashou_profile.save() + logger.info(f"同意退款成功,打手{jiedan_dashou_id}退款订单+1") + + except UserMain.DoesNotExist: + # 打手用户不存在,记录日志但不影响操作 + logger.warning(f"同意退款:打手{jiedan_dashou_id}不存在,跳过打手更新") + except AttributeError: + # 打手扩展表不存在,记录日志但不影响操作 + logger.warning(f"同意退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手更新") + + # 13.3 更新商家扩展表 + if shangjia_id: + try: + # 查询商家用户主表 + shangjia_user = UserMain.objects.get( + yonghuid=shangjia_id, + #user_type='shop' + ) + + # 获取商家扩展表 + shangjia_profile = shangjia_user.shop_profile + + # ✅ 确认字段:tuikuan 退款订单总量 + shangjia_profile.tuikuan += 1 + + # ✅ 确认字段:yue 商家余额(加上订单金额) + shangjia_profile.yue += jine + + shangjia_profile.save() + logger.info(f"同意退款成功,商家{shangjia_id}退款订单+1,余额增加:{jine}") + + except UserMain.DoesNotExist: + # 商家用户不存在,记录日志但不影响操作 + logger.warning(f"同意退款:商家{shangjia_id}不存在,跳过商家更新") + except AttributeError: + # 商家扩展表不存在,记录日志但不影响操作 + logger.warning(f"同意退款:商家{shangjia_id}扩展信息不存在,跳过商家更新") + + # 13.4 更新退款记录表 + try: + # 查询退款记录表 + tuikuan_jilu = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) + + # ✅ 确认字段:sqzhuangtai 申请状态,改为1(成功) + tuikuan_jilu.sqzhuangtai = 1 + + # 可以添加同意理由字段,如:tongyi_liyou + # tuikuan_jilu.tongyi_liyou = tuikuan_liyou + + # ✅ 确认字段:chuliid 处理人ID(使用管理员账号,即手机号) + tuikuan_jilu.chuliid = qianzhanghao + + tuikuan_jilu.save() + logger.info(f"同意退款成功,更新退款记录表,订单:{dingdan_id}") + + except Tuikuanjilu.DoesNotExist: + # 退款记录表不存在,记录日志但不影响主流程 + logger.warning(f"同意退款:订单{dingdan_id}退款记录不存在,跳过更新") + + # 14. 返回成功响应 + return Response({ + 'code': 0, + 'message': '同意退款成功', + 'data': None + }) + + 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 + ) + + +# 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): + """ + 后台同意退款接口(平台发单) + 权限: JWT Token认证 + 管理员身份验证 + 密码验证 + 前端调用: POST /dingdan/adddpttk + 参数: { + "zhanghao": "管理员账号", + "dingdan_id": "订单ID", + "tuikuan_liyou": "退款理由(可选)", + "pay_password": "支付密码(openid)" + } + """ + 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() + tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() + pay_password = request.data.get('pay_password', '').strip() + + if not qianzhanghao or not dingdan_id or not pay_password: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. JWT Token身份验证 + dangqianyonghu = request.user + + # 3. 验证手机号是否匹配 + if str(getattr(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. 验证支付密码(对比openid) + if pay_password != dangqianyonghu.openid: + return Response( + {'code': 401, 'message': '支付密码错误', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 7. 查询订单主表(加锁) + with transaction.atomic(): + try: + dingdan_obj = Dingdan.objects.select_related('pingtai_kuozhan').select_for_update().get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 8. 验证订单状态为1、7、2、8、4(允许退款的状态) + if dingdan_obj.zhuangtai not in [1, 7, 2, 8, 4]: + return Response( + {'code': 400, 'message': '订单状态不允许退款', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 9. 验证订单为平台发单 + if dingdan_obj.fadan_pingtai != 1: + return Response( + {'code': 400, 'message': '此接口只处理平台发单的退款', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 10. 获取订单的重要字段 + jiedan_dashou_id = dingdan_obj.jiedan_dashou_id + jine = dingdan_obj.jine + if jine is None: + return Response( + {'code': 400, 'message': '订单缺少订单金额', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 11. 获取老板ID(从平台订单扩展表) + if not hasattr(dingdan_obj, 'pingtai_kuozhan') or not dingdan_obj.pingtai_kuozhan: + return Response( + {'code': 400, 'message': '订单老板信息不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + laoban_id = dingdan_obj.pingtai_kuozhan.laoban_id + + # 12. 调用微信退款接口(优先使用 wechat_transaction_id) + transaction_id = getattr(dingdan_obj, 'wechat_transaction_id', None) + weixin_refund_result = self.call_weixin_refund( + out_trade_no=dingdan_id, + jine=jine, + transaction_id=transaction_id + ) + + if not weixin_refund_result.get('success'): + logger.error(f"微信退款失败,订单:{dingdan_id},错误:{weixin_refund_result.get('msg')}") + return Response( + {'code': 400, 'message': weixin_refund_result.get('msg', '退款失败')}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 13. 更新订单状态为5(已退款) + dingdan_obj.zhuangtai = 5 + dingdan_obj.save() + + # 14. 更新打手扩展表 + if jiedan_dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.zhuangtai = 1 # 打手状态设为空闲 + dashou_profile.tuikuanliang += 1 + dashou_profile.save() + except (UserMain.DoesNotExist, AttributeError): + logger.warning(f"平台退款:打手{jiedan_dashou_id}不存在或扩展信息缺失,跳过") + + # 15. 更新老板扩展表 + if laoban_id: + try: + laoban_user = UserMain.objects.get(yonghuid=laoban_id) + laoban_profile = laoban_user.boss_profile + laoban_profile.alltui += 1 + laoban_profile.zonge -= jine + if laoban_profile.zonge < 0: + laoban_profile.zonge = 0 + laoban_profile.save() + except (UserMain.DoesNotExist, AttributeError): + logger.warning(f"平台退款:老板{laoban_id}不存在或扩展信息缺失,跳过") + + # 16. 更新收支记录表(id=1) + try: + + #更新支出记录 + from dingdan.utils import update_daily_payout + update_daily_payout(jine) + + + szjilu_obj = Szjilu.objects.get(id=1) + szjilu_obj.zongsy -= jine + szjilu_obj.zongzc += jine + szjilu_obj.jrzc += jine + szjilu_obj.save() + except Szjilu.DoesNotExist: + logger.error(f"平台退款:收支记录表(id=1)不存在,跳过") + except Exception as e: + logger.error(f"更新收支记录表异常: {str(e)}") + + # 17. 更新退款记录表 + try: + tuikuan_jilu, created = Tuikuanjilu.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'dashouid': jiedan_dashou_id if jiedan_dashou_id else '', + 'qingqiuid': laoban_id if laoban_id else '', + 'chuliid': qianzhanghao, + 'liyou': tuikuan_liyou if tuikuan_liyou else '给老板退款', + 'sqzhuangtai': 1, + 'jine': jine, + 'dashou_fencheng': dingdan_obj.dashou_fencheng or 0, + 'jieshao': dingdan_obj.jieshao or '', + 'beizhu': '给老板退款', + 'nicheng': dingdan_obj.nicheng or '', + } + ) + if not created: + tuikuan_jilu.sqzhuangtai = 1 + if tuikuan_liyou: + tuikuan_jilu.liyou = tuikuan_liyou + tuikuan_jilu.chuliid = qianzhanghao + tuikuan_jilu.save() + except Exception as e: + logger.error(f"更新退款记录表异常: {str(e)}") + + logger.info(f"平台退款成功,订单:{dingdan_id},退款单号:{weixin_refund_result.get('refund_id')}") + + return Response({'code': 0, 'message': '退款处理成功', 'data': None}) + + 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 call_weixin_refund(self, out_trade_no, jine, transaction_id=None): + """ + 调用微信退款接口 + :param out_trade_no: 商户订单号 + :param jine: 退款金额 + :param transaction_id: 微信支付订单号(优先使用) + :return: {'success': True/False, 'msg': '消息', 'refund_id': '退款单号'} + """ + try: + # 检查配置 + required_settings = ['WEIXIN_APPID', 'WEIXIN_MCHID', 'WEIXIN_SHANGHUMIYAO', 'WEIXIN_CERT_PATH', 'WEIXIN_KEY_PATH'] + for setting in required_settings: + if not hasattr(settings, setting) or not getattr(settings, setting): + return {'success': False, 'msg': f'缺少微信支付配置: {setting}'} + + # 生成退款单号 + refund_id = f"TK{int(time.time())}{random.randint(1000, 9999)}" + total_fee_in_fen = int(float(jine) * 100) + + # 构造请求参数 + refund_data = { + 'appid': settings.WEIXIN_APPID, + 'mch_id': settings.WEIXIN_MCHID, + 'nonce_str': hashlib.md5(str(time.time()).encode()).hexdigest(), + 'out_refund_no': refund_id, + 'total_fee': total_fee_in_fen, + 'refund_fee': total_fee_in_fen, + 'op_user_id': settings.WEIXIN_MCHID, + 'refund_account': 'REFUND_SOURCE_UNSETTLED_FUNDS' + } + # 优先使用 transaction_id,否则用 out_trade_no + if transaction_id: + refund_data['transaction_id'] = transaction_id + else: + refund_data['out_trade_no'] = out_trade_no + + # 生成签名 + sorted_params = sorted(refund_data.items()) + sign_string = '&'.join([f"{k}={v}" for k, v in sorted_params if v]) + sign_string += f"&key={settings.WEIXIN_SHANGHUMIYAO}" + sign = hashlib.md5(sign_string.encode()).hexdigest().upper() + refund_data['sign'] = sign + + xml_data = self.dict_to_xml(refund_data) + url = "https://api.mch.weixin.qq.com/secapi/pay/refund" + response = requests.post( + url, + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + cert=(settings.WEIXIN_CERT_PATH, settings.WEIXIN_KEY_PATH), + timeout=30 + ) + + if response.status_code == 200: + result = self.xml_to_dict(response.content) + if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': + logger.info(f"微信退款成功,订单:{out_trade_no},退款单号:{refund_id}") + return {'success': True, 'msg': '退款申请提交成功', 'refund_id': refund_id} + else: + error_msg = result.get('err_code_des', result.get('return_msg', '退款申请失败')) + logger.error(f"微信退款失败,订单:{out_trade_no},错误:{error_msg}") + return {'success': False, 'msg': f'微信退款失败: {error_msg}'} + else: + logger.error(f"微信退款接口调用失败,状态码:{response.status_code}") + return {'success': False, 'msg': '微信退款接口调用失败'} + + except requests.exceptions.Timeout: + logger.error(f"微信退款接口请求超时,订单:{out_trade_no}") + return {'success': False, 'msg': '微信退款接口请求超时'} + except Exception as e: + logger.error(f"调用微信退款接口异常: {str(e)}") + return {'success': False, 'msg': f'退款处理异常: {str(e)}'} + + def dict_to_xml(self, dict_data): + xml = [""] + for k, v in dict_data.items(): + xml.append(f"<{k}>{v}") + xml.append("") + return "".join(xml) + + def xml_to_dict(self, xml_data): + try: + root = ET.fromstring(xml_data) + return {child.tag: child.text for child in root} + except ET.ParseError as e: + logger.error(f"XML解析失败: {str(e)}") + return {'return_code': 'FAIL', 'return_msg': 'XML解析失败'} + + +logger = logging.getLogger(__name__) + + +class DashouHuoquLeixingView(APIView): + """ + 打手获取商品类型接口 - 抢单页面使用 + 接口地址:POST /dingdan/dsqdhqddlx + 功能:返回所有正常的商品类型,已按排序权重排序 + + 返回格式(与前端原数据格式完全一致): + { + "code": 200, + "msg": "成功", + "data": { + "list": [ + { + "id": 1, + "jieshao": "王者荣耀", + "tupian_url": "leixing/wzry.jpg", + "yaoqiuleixing": null, + "huiyuan_id": null, + "yongjin": null, + "paixu": 100, + "shenhezhuangtai": 1 + }, + // ... 更多类型 + ] + } + } + """ + + # 🔴 使用JWT Token认证 + permission_classes = [IsAuthenticated] + + def post(self, request): + """ + 处理POST请求,返回商品类型列表 + + 请求参数:无特殊参数,只验证用户身份 + + 安全验证: + 1. JWT Token验证用户身份 + 2. 只返回 shenhezhuangtai=1(正常状态)的类型 + 3. 数据已排序:paixu降序 → id升序 + """ + try: + logger.info(f"打手获取商品类型请求 - 用户ID: {request.user.yonghuid}") + + # 🔴 导入商品类型模型 + from shangpin.models import ShangpinLeixing + + # 🔴 查询所有正常状态的商品类型 + # 条件:shenhezhuangtai=1(正常状态) + # 排序:先按 paixu 降序(数值大的在前),再按 id 升序 + leixing_queryset = ShangpinLeixing.objects.filter( + shenhezhuangtai=1 # 🔴 只取正常状态,排除审核专用 + ).order_by('-paixu', 'id') + + # 🔴 构建返回数据列表 + leixing_list = [] + for leixing in leixing_queryset: + leixing_list.append({ + "id": leixing.id, + "jieshao": leixing.jieshao or "", + "tupian_url": leixing.tupian_url or "", + "yaoqiuleixing": leixing.yaoqiuleixing, + "huiyuan_id": leixing.huiyuan_id, + "yongjin": float(leixing.yongjin) if leixing.yongjin else None, + "paixu": leixing.paixu or 0, + "shenhezhuangtai": leixing.shenhezhuangtai or 1 + }) + + # 🔴 记录日志 + logger.info(f"返回商品类型数据:{len(leixing_list)}条,用户: {request.user.yonghuid}") + + # 🔴 返回成功响应 + # 注意:保持与前端原数据格式完全一致 + return Response({ + "code": 200, + "msg": "成功", + "data": { + "list": leixing_list # 🔴 保持原字段名 "list" + } + }, 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": f"服务器内部错误: {str(e)}", + "data": None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +logger = logging.getLogger(__name__) + +from peizhi.models import Club ,OrderTypeMapping + +logger = logging.getLogger(__name__) + + +class PartnerSyncOrderView(APIView): + """ + 接收对方平台派单通知接口 + 对方平台商家派单成功后,异步调用此接口,将订单信息同步过来。 + """ + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 验证 token + token = request.data.get('token') + if not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): + logger.warning(f"跨平台派单 token 无效: {token}") + return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) + + # 2. 获取必要参数 + our_club_id = request.data.get('our_club_id') # 对方平台发送的我方俱乐部ID(即我方在对方表中的 partner_club_id) + partner_club_id = request.data.get('partner_club_id') # 对方平台的俱乐部ID(对方自己的ID) + partner_order_id = request.data.get('partner_order_id') + jine = request.data.get('jine') + dashou_fencheng = request.data.get('dashou_fencheng') + jieshao = request.data.get('jieshao') + beizhu = request.data.get('beizhu', '') + create_time_str = request.data.get('create_time') + nicheng = request.data.get('nicheng') + leixing_id = request.data.get('leixing_id') # 对方平台的商品类型ID + partner_yonghu_id = request.data.get('user_id') # 对方下单用户ID + + # 必需字段校验 + required_fields = [our_club_id, partner_club_id, partner_order_id, jine, dashou_fencheng, + jieshao, create_time_str, nicheng, leixing_id] + if not all(required_fields): + logger.error(f"跨平台派单参数缺失: {request.data}") + return Response({'code': 400, 'message': 'Missing required parameters'}, status=status.HTTP_400_BAD_REQUEST) + + # 3. 验证我方俱乐部是否存在且同意接收订单 + try: + my_club = Club.objects.get(partner_club_id=our_club_id) + except Club.DoesNotExist: + logger.warning(f"我方俱乐部不存在,partner_club_id={our_club_id}") + return Response({'code': 404, 'message': 'Club not found'}, status=status.HTTP_404_NOT_FOUND) + + if not my_club.is_receive_enabled: + logger.warning(f"我方俱乐部 {my_club.club_id} 未同意接收订单") + return Response({'code': 403, 'message': 'Receiving not allowed'}, status=status.HTTP_403_FORBIDDEN) + + # 4. 查找订单类型映射 + try: + mapping = OrderTypeMapping.objects.get( + partner_club_id=our_club_id, # 对方在我方的标识 + partner_type_id=leixing_id + ) + our_type_id = mapping.our_type_id + except OrderTypeMapping.DoesNotExist: + logger.error(f"未找到订单类型映射: 对方俱乐部={our_club_id}, 对方类型ID={leixing_id}") + return Response({'code': 400, 'message': 'Order type mapping not found'}, status=status.HTTP_400_BAD_REQUEST) + + # 5. 获取我方商品类型信息(用于获取会员ID、佣金等) + try: + our_leixing = ShangpinLeixing.objects.get(id=our_type_id) + except ShangpinLeixing.DoesNotExist: + logger.error(f"我方商品类型不存在: {our_type_id}") + return Response({'code': 400, 'message': 'Local product type not found'}, status=status.HTTP_400_BAD_REQUEST) + + # 6. 幂等检查:如果已存在相同 partner_order_id 的订单,直接返回(附带 status='existed') + existing = Dingdan.objects.filter(partner_order_id=partner_order_id).first() + if existing: + logger.info(f"跨平台派单幂等返回: partner_order_id={partner_order_id} 已存在") + return Response({ + 'code': 0, + 'data': { + 'local_order_id': existing.dingdan_id, + 'status': 'existed' # 明确告知已存在 + } + }) + + # 7. 生成我方订单ID(确保唯一性) + 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}" + + # 8. 获取代理商家头像(用于订单图片) + agent_user = UserMain.objects.filter(yonghuid=settings.AGENT_SHANGJIA_ID).first() + agent_avatar = agent_user.avatar if agent_user else '' + + # 9. 计算我方实际打手分成金额:对方传来的打手分成 × 我方接收费率 + try: + actual_dashou_fencheng = Decimal(dashou_fencheng) * my_club.receive_rate + except Exception: + actual_dashou_fencheng = Decimal(dashou_fencheng) + + # 10. 事务创建订单 + try: + with transaction.atomic(): + new_id = generate_dingdan_id() + # 创建主订单 + dingdan = Dingdan.objects.create( + dingdan_id=new_id, + zhuangtai=1, # 普通订单(待接单) + fadan_pingtai=2, # 商家发单 + jine=jine, # 原始订单金额 + dashou_fencheng=actual_dashou_fencheng, # 调整后的打手分成 + jieshao=jieshao, + beizhu=beizhu, + create_time=create_time_str, + yaoqiuleixing=our_leixing.yaoqiuleixing, + nicheng=nicheng, + huiyuan_id=our_leixing.huiyuan_id, + yongjin=our_leixing.yongjin, + leixing_id=our_type_id, + partner_order_id=partner_order_id, # 存储对方订单ID + is_cross=1, + dispatch_type=2, # 对方派单 + partner_club_id=our_club_id, # 对方俱乐部ID + tupian=agent_avatar, # 使用代理商家头像作为订单图片 + ) + # 创建商家扩展表(使用代理商家) + DingdanShangjia.objects.create( + dingdan=dingdan, + shangjia_id=settings.AGENT_SHANGJIA_ID, + sjnicheng='跨平台代理', + partner_yonghu_id=partner_yonghu_id or '', + ) + + # 成功创建后更新每日派单统计(方向=对方派单) + try: + update_daily_dispatch_stat( + direction=2, # 对方派单 + partner_club_id=our_club_id, + dispatch_amount=Decimal(jine), # 统计使用原始订单金额 + + ) + except Exception as e: + logger.error(f"更新派单统计失败: {e}", exc_info=True) + + 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 = { + '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) # 瞬间返回,不阻塞请求 + + + return Response({ + 'code': 0, + 'data': { + 'local_order_id': new_id, + 'status': 'created' + } + }) + + except Exception as e: + logger.error(f"跨平台订单同步失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'message': 'Internal error'}, 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 '游戏订单' + + +from dingdan.utils import update_daily_dispatch_stat + +logger = logging.getLogger(__name__) + + +class PartnerCheckClaimView(APIView): + """ + 接收对方平台抢单询问(对方打手抢我方派出的订单) + 如果允许抢单: + 1. 订单状态改为2(已接单) + 2. 记录对方打手ID、对方俱乐部ID、对方订单ID + 3. 更新派单成交统计(success_count/success_amount) + """ + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + token = request.data.get('token') + if not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): + logger.warning(f"token无效: {token}") + return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) + + wofangdingdan_id = request.data.get('wofangdingdan_id') + partner_order_id = request.data.get('partner_order_id') + our_club_id = request.data.get('our_club_id') + dashou_id = request.data.get('dashou_id') + sj_id = request.data.get('sj_id') + + if not all([wofangdingdan_id, partner_order_id, our_club_id, dashou_id, sj_id]): + logger.error(f"参数缺失: {request.data}") + return Response({'code': 400, 'message': 'Missing parameters'}, status=status.HTTP_400_BAD_REQUEST) + + try: + with transaction.atomic(): + # 查询我方俱乐部配置 + try: + club = Club.objects.select_for_update().get(partner_club_id=our_club_id) + except Club.DoesNotExist: + logger.warning(f"俱乐部配置不存在 partner_club_id={our_club_id}") + return Response({'code': 0, 'data': {'can_claim': False}}) + + if not club.is_interop_enabled: + logger.warning(f"俱乐部 {club.club_id} 未开启互通") + return Response({'code': 0, 'data': {'can_claim': False}}) + + # 锁定订单 + try: + order = Dingdan.objects.select_for_update().get(dingdan_id=wofangdingdan_id) + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {wofangdingdan_id}") + return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND) + + # 只有状态1可抢 + if order.zhuangtai != 1: + logger.info(f"订单 {order.dingdan_id} 状态 {order.zhuangtai} 不可抢") + return Response({'code': 0, 'data': {'can_claim': False}}) + + # 商品类型检查 + try: + leixing = ShangpinLeixing.objects.get(id=order.leixing_id) + except ShangpinLeixing.DoesNotExist: + logger.warning(f"商品类型不存在: {order.leixing_id}") + return Response({'code': 0, 'data': {'can_claim': False}}) + + if not leixing.is_cross_enabled: + logger.warning(f"商品类型 {leixing.jieshao} 未开启跨平台") + return Response({'code': 0, 'data': {'can_claim': False}}) + + # 执行抢单:状态改为2(已接单) + order.zhuangtai = 2 + order.jiedan_dashou_id = dashou_id + order.partner_club_id = our_club_id + order.partner_order_id = partner_order_id + if order.is_cross != 1: + order.is_cross = 1 + # dispatch_type 保持原值(1-我方派单),不变 + order.save() + + # 更新派单成交统计(我方派单被成功接单) + update_daily_dispatch_stat( + direction=1, + partner_club_id=our_club_id, + claimed_amount=order.jine, + + ) + + logger.info(f"对方抢单成功: 订单 {order.dingdan_id}, 对方打手 {dashou_id}") + return Response({'code': 0, 'data': {'can_claim': True}}) + + except Exception as e: + logger.error(f"异常: {e}", exc_info=True) + return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class PartnerNotifyClaimView(APIView): + """ + 接收对方平台抢单成功通知(对方派单被他们自己的打手抢了) + 我方根据 partner_order_id 找到订单,如果状态为1则改为20,不更新任何统计。 + """ + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + token = request.data.get('token') + if not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): + logger.warning(f"token无效: {token}") + return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) + + partner_order_id = request.data.get('partner_order_id') + if not partner_order_id: + logger.error("缺少 partner_order_id") + return Response({'code': 400, 'message': 'Missing partner_order_id'}, status=status.HTTP_400_BAD_REQUEST) + + try: + with transaction.atomic(): + order = Dingdan.objects.select_for_update().get(partner_order_id=partner_order_id) + # 只有状态为1(未被抢)时才改为20 + if order.zhuangtai == 1: + order.zhuangtai = 20 + order.save() + logger.info(f"订单 {order.dingdan_id} 状态改为20(被其他俱乐部抢走)") + else: + logger.info(f"订单 {order.dingdan_id} 当前状态 {order.zhuangtai},无需修改") + return Response({'code': 0, 'message': 'OK'}) + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在 partner_order_id={partner_order_id}") + return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"异常: {e}", exc_info=True) + return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class PartnerRefundNotifyView(APIView): + """ + 接收对方平台退款通知接口 + 对方平台商家申请退款时,调用此接口通知我方,我方将本地对应订单改为退款状态。 + 请求方式: POST + 接口地址: /dingdan/partner_refund_notify + 请求参数: + token: 认证令牌,需与settings.CROSS_PLATFORM_TOKEN一致 + order_id: 对方平台的订单ID(即我方记录的 partner_order_id) + 返回: + code: 0成功,其他失败 + message: 描述信息 + """ + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 验证token + token = request.data.get('token') + if not hmac.compare_digest(str(token), settings.CROSS_PLATFORM_TOKEN): + logger.warning(f"跨平台退款通知 token 无效: {token}") + return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN) + + order_id = request.data.get('order_id') # 对方平台的订单ID(即我方的 partner_order_id) + if not order_id: + logger.error("跨平台退款通知参数缺失: order_id") + return Response({'code': 400, 'message': 'Missing order_id'}, status=status.HTTP_400_BAD_REQUEST) + + try: + with transaction.atomic(): + # 2. 根据 partner_order_id 查找订单(必须存在且为跨平台订单) + order = Dingdan.objects.select_for_update().get(partner_order_id=order_id, is_cross=1) + # 如果订单已完成,不允许退款 + if order.zhuangtai == 3: + logger.warning(f"跨平台退款通知:订单 {order_id} 已完成,不可退款") + return Response({'code': 400, 'message': 'Order already completed, cannot refund'}, status=status.HTTP_400_BAD_REQUEST) + + # 3. 如果订单状态不是5,则改为5(已退款),并更新打手信息 + if order.zhuangtai != 5: + # 记录原打手ID + old_dashou_id = order.jiedan_dashou_id + if old_dashou_id: + try: + # 查询打手用户主表 + dashou_user = UserMain.objects.get(yonghuid=old_dashou_id) + # 通过反向关系获取打手扩展表 + if hasattr(dashou_user, 'dashou_profile'): + dashou_profile = dashou_user.dashou_profile + dashou_profile.zhuangtai = 1 # 恢复打手状态 + dashou_profile.tuikuanliang += 1 # 退款订单量+1 + dashou_profile.save() + logger.info(f"退款通知:打手 {old_dashou_id} 状态已恢复,退款订单+1") + except UserMain.DoesNotExist: + logger.warning(f"退款通知:打手用户不存在 {old_dashou_id}") + except Exception as e: + logger.error(f"退款通知:更新打手信息失败 {str(e)}") + + # 更新订单状态为5 + order.zhuangtai = 5 + # 可选:清空接单打手ID + #order.jiedan_dashou_id = None + order.save() + logger.info(f"跨平台退款通知成功:订单 {order_id} 已改为退款状态") + else: + logger.info(f"跨平台退款通知:订单 {order_id} 已是退款状态,无需重复处理") + + return Response({'code': 0, 'message': 'Refund processed successfully'}) + + except Dingdan.DoesNotExist: + logger.warning(f"跨平台退款通知:订单不存在或不是跨平台订单,partner_order_id={order_id}") + return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"跨平台退款通知异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +logger = logging.getLogger(__name__) + + +class AdJuJueJieSuan(APIView): + """ + 后台拒绝结算接口(平台发单) + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddjjjs + 参数: { + "zhanghao": "管理员账号(手机号)", + "dingdan_id": "订单ID", + "tkly": "拒绝结算理由(必填)" + } + """ + 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() + tkly = request.data.get('tkly', '').strip() + + if not all([qianzhanghao, dingdan_id, tkly]): + return Response( + {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id, tkly)', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. JWT用户验证 + dangqianyonghu = request.user + + # 3. 验证手机号匹配(管理员账号通常为手机号) + if 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: + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 6. 校验订单条件:平台发单(1)且状态为2或8 + if dingdan.fadan_pingtai != 1 or dingdan.zhuangtai not in [2, 8]: + return Response( + {'code': 400, 'message': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + jiedan_dashou_id = dingdan.jiedan_dashou_id + if not jiedan_dashou_id: + return Response( + {'code': 400, 'message': '订单暂无接单打手', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + with transaction.atomic(): + # 7. 更新订单状态为5(已退款),并保存拒绝理由 + dingdan.zhuangtai = 5 + dingdan.tkly = tkly + dingdan.save() + + # 8. 更新打手扩展表 + 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() + except (UserMain.DoesNotExist, AttributeError) as e: + logger.warning(f"拒绝结算:打手{jiedan_dashou_id}信息更新失败 - {str(e)}") + + return Response({'code': 0, 'message': '拒绝结算成功', 'data': None}) + + 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 AdZhuanYiDaTing(APIView): + """ + 后台转移大厅接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddzydt + 参数: { + "zhanghao": "管理员账号(手机号)", + "dingdan_id": "订单ID" + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not all([qianzhanghao, dingdan_id]): + return Response( + {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id)', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + dangqianyonghu = request.user + + if 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: + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 校验订单状态:允许2、8、4 + if dingdan.zhuangtai not in [2, 8, 4]: + return Response( + {'code': 400, 'message': '仅状态为进行中(2)、结算中(8)或退款审核(4)的订单可转移大厅', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + jiedan_dashou_id = dingdan.jiedan_dashou_id + + with transaction.atomic(): + # 更新订单:状态改为1(下单中),清空接单打手ID + dingdan.zhuangtai = 1 + dingdan.jiedan_dashou_id = None + dingdan.save() + + # 如果有接单打手,更新其扩展表 + if jiedan_dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.zhuangtai = 1 + dashou_profile.tuikuanliang += 1 + dashou_profile.save() + except (UserMain.DoesNotExist, AttributeError) as e: + logger.warning(f"转移大厅:打手{jiedan_dashou_id}信息更新失败 - {str(e)}") + + return Response({'code': 0, 'message': '转移大厅成功', 'data': None}) + + 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 AdQuXiaoZhiDing(APIView): + """ + 后台取消指定接口 + 权限: JWT Token认证 + 管理员身份验证 + 前端调用: POST /dingdan/adddqxzd + 参数: { + "zhanghao": "管理员账号(手机号)", + "dingdan_id": "订单ID" + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + qianzhanghao = request.data.get('zhanghao', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not all([qianzhanghao, dingdan_id]): + return Response( + {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id)', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + dangqianyonghu = request.user + + if 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: + dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 校验订单状态:必须为7(指定中) + if dingdan.zhuangtai != 7: + return Response( + {'code': 400, 'message': '仅状态为指定中(7)的订单可取消指定', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + with transaction.atomic(): + # 更新订单:状态改为1(下单中),清空指定ID + dingdan.zhuangtai = 1 + dingdan.zhiding_id = None + dingdan.save() + + return Response({'code': 0, 'message': '取消指定成功', 'data': None}) + + 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 DashouXiugaiView(APIView): + """ + 打手修改订单交付内容接口 + 请求方式:POST + 请求路径:/dingdan/dsxiugaidd + 权限:需携带有效JWT Token,且用户为打手身份 + 请求参数: + dingdan_id : 订单ID(必填) + liuyan : 新的留言内容(可选,最长50字) + deleted_images : 要删除的图片URL列表(数组,每个元素为完整COS URL) + new_images : 新增的图片URL列表(数组,每个元素为完整COS URL) + 返回: + 成功:{"code":0,"msg":"修改成功","data":null} + 失败:返回相应错误码和提示 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 获取请求数据 + data = request.data + dingdan_id = data.get('dingdan_id', '') + liuyan = data.get('liuyan', '') + deleted_images = data.get('deleted_images', []) + new_images = data.get('new_images', []) + + # ---------- 参数校验 ---------- + if not dingdan_id: + return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + if not isinstance(deleted_images, list): + return Response({'code': 2, 'msg': 'deleted_images 必须为数组', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + if not isinstance(new_images, list): + return Response({'code': 3, 'msg': 'new_images 必须为数组', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + if liuyan and len(liuyan) > 50: + return Response({'code': 4, 'msg': '留言最多50个字', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # ---------- 用户身份验证 ---------- + current_user = request.user + yonghuid = current_user.yonghuid + + # 检查是否为打手 + try: + dashou_profile = current_user.dashou_profile # 假设User模型有related_name='dashou_profile' + except UserDashou.DoesNotExist: + return Response({'code': 5, 'msg': '用户不是打手身份', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + # ---------- 订单查询及权限验证 ---------- + try: + # 订单必须属于该打手,且状态为4(退款中)或8(结算中) + dingdan = Dingdan.objects.get( + dingdan_id=dingdan_id, + jiedan_dashou_id=yonghuid, + zhuangtai__in=[4, 8] # 只允许修改退款中或结算中的订单 + ) + except Dingdan.DoesNotExist: + return Response({'code': 6, 'msg': '订单不存在或没有修改权限', 'data': None}, + status=status.HTTP_403_FORBIDDEN) + + # ---------- 执行修改(使用事务保证原子性) ---------- + try: + with transaction.atomic(): + # 1. 处理删除的图片:从 Dashoutupian 表中删除记录 + if deleted_images: + # 根据订单ID、打手ID和图片URL删除记录 + delete_count, _ = Dashoutupian.objects.filter( + dingdan_id=dingdan_id, + dashou=yonghuid, + tupian__in=deleted_images + ).delete() + # 可选:记录删除数量 + logger.info(f"订单 {dingdan_id} 删除图片 {delete_count} 张") + + # 2. 处理新增的图片:插入新记录 + if new_images: + # 批量创建 + new_records = [] + for url in new_images: + new_records.append(Dashoutupian( + dingdan_id=dingdan_id, + dashou=yonghuid, + tupian=url, + create_time=timezone.now() + )) + Dashoutupian.objects.bulk_create(new_records) + logger.info(f"订单 {dingdan_id} 新增图片 {len(new_records)} 张") + + # 3. 更新订单留言(如果发生变化) + # 注意:即使留言为空字符串,也允许更新,表示清空留言 + if 'liuyan' in data: # 判断是否传了liuyan字段(可能为空字符串) + dingdan.dashou_liuyan = liuyan + dingdan.update_time = timezone.now() + dingdan.save(update_fields=['dashou_liuyan', 'update_time']) + + # 全部成功,返回成功响应 + return Response({ + 'code': 0, + 'msg': '修改成功', + 'data': None + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"修改订单失败: {str(e)}", exc_info=True) + return Response({ + 'code': 7, + 'msg': '服务器内部错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +from django.core.exceptions import ObjectDoesNotExist +from dingdan.tongzhi_tasks import dingdan_guangbo + +class ZxsjghdsView(APIView): + """ + 商家更换打手接口 + 路径:POST /dingdan/zxsjghds + 权限:仅限已认证商家(通过JWT) + 功能: + 1. 将原订单状态改为已退款(5),释放接单打手(若有) + 2. 更新打手统计(退款量、状态、每日统计) + 3. 写入退款记录 + 4. 若为跨平台订单,通知对方平台退款(必须成功) + 5. 基于原订单生成新订单(状态直接为1),复制核心数据 + 6. 不操作商家余额、退款次数等(替换订单) + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # ========== 1. 获取当前商家用户 ========== + current_user = request.user + try: + yonghuid = current_user.yonghuid + except AttributeError: + return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # ========== 2. 提取订单ID ========== + dingdan_id = request.data.get('dingdan_id', '').strip() + if not dingdan_id: + return Response({'code': 2, 'msg': '订单ID不能为空'}, status=status.HTTP_400_BAD_REQUEST) + + # ========== 3. 查询订单并校验权限 ========== + try: + # 关联查询商家扩展表,确保当前商家是该订单的发布者 + shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').get( + dingdan__dingdan_id=dingdan_id, + shangjia_id=yonghuid + ) + except DingdanShangjia.DoesNotExist: + return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN) + + old_order = shangjia_kuozhan.dingdan + + # ========== 4. 状态校验(仅允许进行中或结算中) ========== + if old_order.zhuangtai not in [2, 8]: + return Response({'code': 4, 'msg': '当前订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST) + + # ========== 5. 收集订单关键数据(用于新订单复制) ========== + # 这些字段将在退款处理后用于创建新订单 + 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 + fadan_pingtai = old_order.fadan_pingtai + + # 跨平台相关 + 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 + + dashou_id = old_order.jiedan_dashou_id # 可能为空 + + # ========== 6. 退款处理(事务块1:退款原订单 + 打手统计 + 跨平台通知) ========== + try: + with transaction.atomic(): + # 6.1 订单状态更新为已退款 + old_order.zhuangtai = 5 + old_order.save(update_fields=['zhuangtai']) + + # 6.2 释放打手 + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1 + dashou_profile.zhuangtai = 1 # 空闲 + dashou_profile.save(update_fields=['tuikuanliang', 'zhuangtai']) + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"打手 {dashou_id} 不存在或扩展表缺失,跳过更新") + + # 6.3 更新退款记录 + Tuikuanjilu.objects.update_or_create( + dingdan_id=dingdan_id, + defaults={ + 'dashouid': dashou_id or '', + 'qingqiuid': yonghuid, # 申请人为当前商家 + 'chuliid': yonghuid, # 处理人也是商家自己 + 'liyou': '商家更换打手', + 'sqzhuangtai': 1, # 状态 1=成功 + 'jine': jine or 0, + 'dashou_fencheng': dashou_fencheng or 0, + 'jieshao': jieshao or '', + 'beizhu': '更换打手系统自动退款', + 'nicheng': nicheng or '', + } + ) + + # 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, + action=3 # 3=退款 + ) + except Exception as e: + logger.error(f"更新打手每日统计失败: {e}") + # 统计失败不应阻断主流程,但可记录 + + # 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("我方俱乐部配置不存在") + 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.rstrip('/') + + notify_url = f"{partner_domain}/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: + transaction.set_rollback(True) + logger.error(f"对方平台接口HTTP错误: {resp.status_code}") + return Response({'code': 500, 'msg': '通知对方平台失败'}) + 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')}"}) + + # 更新跨平台扩展数据 + 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)}'}) + + except Exception as e: + logger.error(f"退款处理事务失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '退款处理失败'}) + + # ========== 7. 生成新订单(事务块2) ========== + try: + with transaction.atomic(): + # 7.1 生成新的订单ID + 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}" + + new_dingdan_id = generate_dingdan_id() + # 确保唯一 + while Dingdan.objects.filter(dingdan_id=new_dingdan_id).exists(): + new_dingdan_id = generate_dingdan_id() + + # 7.2 创建新订单(复制数据,状态为 1) + new_order = Dingdan.objects.create( + dingdan_id=new_dingdan_id, + zhuangtai=1, # 已下单 + fadan_pingtai=fadan_pingtai, + jine=jine, + dashou_fencheng=dashou_fencheng, + leixing_id=leixing_id, + yaoqiuleixing=yaoqiuleixing, + huiyuan_id=huiyuan_id, + yongjin=yongjin, + jieshao=jieshao, + nicheng=nicheng, + beizhu=beizhu, + user1_id=user1_id, # 商家标识 + tupian=tupian, + # 不继承:zhiding_id, jiedan_dashou_id, dashou_liuyan 等 + ) + + # 7.3 创建商家扩展表(关联新订单) + DingdanShangjia.objects.create( + dingdan=new_order, + shangjia_id=yonghuid, + sjnicheng=shangjia_kuozhan.sjnicheng or '', + ) + + # 7.4 异步通知打手(广播新订单) + try: + order_info = { + 'dingdan_id': new_dingdan_id, + 'game_type': self._get_game_type_name(leixing_id), + 'amount': str(jine), + 'order_desc': (jieshao or '')[:50], + } + dingdan_guangbo.delay(order_info) + except Exception as e: + logger.error(f"广播通知失败: {e}") + + # 7.5 跨平台同步(如果适用) + try: + sync_order_to_partners(new_dingdan_id) + except Exception as e: + logger.error(f"跨平台同步失败: {e}") + + # 注意:此处不更新商家余额、发布量等统计,因为是替换订单 + + logger.info(f"商家{yonghuid}更换打手成功,旧订单{dingdan_id}已退款,新订单{new_dingdan_id}") + + return Response({ + 'code': 0, + 'msg': '更换成功', + 'data': { + 'xin_dingdan_id': new_dingdan_id, + } + }) + + except Exception as e: + logger.error(f"生成新订单事务失败: {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 '游戏订单' + + +# ===================== 视图:商家罚款申请 ===================== +# 文件路径:dingdan/views.py 或单独文件,根据项目结构放置 + + +class ShangjiaFakuanApplyView(APIView): + """ + 商家罚款申请接口 + 路径:POST /dingdan/sjfksq + 权限:JWT认证的商家用户 + 参数: + dingdan_id - 订单ID (必填) + chufa_liyou - 罚款原因 (必填) + fakuanjine - 罚款金额,元 (必填) + yingxiang_qiangdan - 是否影响抢单 1是 0否 (必填) + 说明: + 1. 自动从订单中获取被打手ID,防止商家篡改目标。 + 2. 同一订单同一打手不允许重复罚款。 + 3. 跨平台订单若为我方派单,则通知对方平台处罚;否则本地生成罚单。 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # ========== 1. 身份验证 ========== + current_user = request.user + try: + shangjia_id = current_user.yonghuid # 商家的用户ID + except AttributeError: + return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # ========== 2. 参数提取与校验 ========== + dingdan_id = request.data.get('dingdan_id', '').strip() + chufa_liyou = request.data.get('chufa_liyou', '').strip() + fakuanjine = request.data.get('fakuanjine', 0) + yingxiang_qiangdan = int(request.data.get('yingxiang_qiangdan', 1)) + + if not dingdan_id or not chufa_liyou or not fakuanjine: + return Response({'code': 2, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) + + try: + fakuanjine = float(fakuanjine) + if fakuanjine <= 0: + raise ValueError + except (ValueError, TypeError): + return Response({'code': 2, 'msg': '罚款金额格式错误'}, status=status.HTTP_400_BAD_REQUEST) + + # ========== 3. 获取订单,校验归属商家 ========== + try: + # 关联查询商家扩展表,确保订单属于当前商家 + shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').get( + dingdan__dingdan_id=dingdan_id, + shangjia_id=shangjia_id + ) + order = shangjia_kuozhan.dingdan + except DingdanShangjia.DoesNotExist: + return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN) + + # ========== 4. 获取被打手ID ========== + dashou_id = order.jiedan_dashou_id + if not dashou_id: + return Response({'code': 4, 'msg': '该订单暂无接单打手,无法罚款'}, status=status.HTTP_400_BAD_REQUEST) + + # ========== 5. 重复罚款检查 ========== + if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): + return Response({'code': 5, 'msg': '该打手在此订单已被罚款,不能重复申请'}, status=status.HTTP_400_BAD_REQUEST) + + # ========== 6. 跨平台信息 ========== + is_cross = order.is_cross + partner_order_id = order.partner_order_id or '' + partner_club_id = order.partner_club_id or '' + dispatch_type = order.dispatch_type or 0 # 1=我方派单,2=对方派单 + + # 是否需要通知对方平台:我方派单且存在对方订单ID + need_notify_partner = is_cross and partner_order_id and dispatch_type == 1 + + # ========== 7. 本地创建罚单(可重用) ========== + def create_local_fadan(): + Fadan.objects.create( + beichufa_id=dashou_id, + shenqing_chufa=shangjia_id, # 申请人为当前商家 + shenfen=1, # 被打者身份:1=打手 + chufaliyou=chufa_liyou, + fakuanjine=fakuanjine, + guanliandingdan_id=dingdan_id, + zhuangtai=1, # 待缴纳 + yingxiang_qiangdan=yingxiang_qiangdan, + shenqingren_shenfen=5, # 申请人是商家 + partner_club_id='', + ) + + # ========== 8. 本地处罚分支 ========== + if not need_notify_partner: + with transaction.atomic(): + create_local_fadan() + logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}罚款成功,金额{fakuanjine}") + return Response({'code': 0, 'msg': '罚款申请已提交'}) + + # ========== 9. 跨平台通知分支 ========== + try: + # 9.1 获取我方俱乐部ID + my_club = ClubConfig.objects.filter(is_self=1).first() + if not my_club: + return Response({'code': 500, 'msg': '系统配置错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + self_club_id = my_club.club_id + + # 9.2 获取对方平台域名 + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain.rstrip('/') + except ObjectDoesNotExist: + return Response({'code': 500, 'msg': '对方平台信息未配置'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 9.3 构造通知参数 + notify_url = f"{partner_domain}/houtai/kffkdssq_notify" # 与后台接口保持一致 + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id, + 'chufaliyou': chufa_liyou, + 'fakuanjine': fakuanjine, + 'yingxiang_qiangdan': yingxiang_qiangdan, + 'shenqingren_shenfen': 5, # 商家身份 + 'operator': shangjia_id, # 操作人ID + } + + # 9.4 带重试的通知 + timeout = 3 + retries = 2 + success = False + for attempt in range(retries): + try: + resp = requests.post(notify_url, json=payload, timeout=timeout) + if resp.status_code == 200: + resp_data = resp.json() + if resp_data.get('code') == 0: + success = True + break + else: + logger.warning(f"对方平台罚款通知失败,响应: {resp_data}") + else: + logger.warning(f"对方平台HTTP错误: {resp.status_code}") + except Exception as e: + logger.warning(f"通知对方平台异常,第{attempt+1}次尝试: {e}") + continue + + if not success: + return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 9.5 通知成功后,本地也创建罚单(标记跨平台) + with transaction.atomic(): + Fadan.objects.create( + beichufa_id=dashou_id, + shenqing_chufa=shangjia_id, + shenfen=1, + chufaliyou=chufa_liyou, + fakuanjine=fakuanjine, + guanliandingdan_id=dingdan_id, + zhuangtai=1, + yingxiang_qiangdan=yingxiang_qiangdan, + shenqingren_shenfen=5, + partner_club_id=partner_club_id, # 记录对方俱乐部ID + ) + + logger.info(f"商家{shangjia_id}跨平台罚款成功: {dingdan_id}") + return Response({'code': 0, 'msg': '罚款已提交至对方平台'}) + + +class ShangjiaFakuaiXiugaiView(APIView): + """ + 商家端统一修改接口:罚款修改/撤销/再次申请,订单退款撤销/修改理由 + URL: /dingdan/sjfkxgycx + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + user = request.user + # 验证商家身份 + try: + shangjia = user.shop_profile + except UserShangjia.DoesNotExist: + return Response({'code': 403, 'msg': '您不是商家'}, status=status.HTTP_403_FORBIDDEN) + if shangjia.zhuangtai != 1: + return Response({'code': 403, 'msg': '商家状态异常'}, status=status.HTTP_403_FORBIDDEN) + + operation = request.data.get('operation') + dingdan_id = request.data.get('dingdan_id') + if not dingdan_id: + return Response({'code': 400, 'msg': '缺少订单ID'}, status=status.HTTP_400_BAD_REQUEST) + + # 验证订单属于该商家 + try: + dingdan = 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 not hasattr(dingdan, 'shangjia_kuozhan') or dingdan.shangjia_kuozhan.shangjia_id != user.yonghuid: + return Response({'code': 403, 'msg': '无权操作此订单'}, status=status.HTTP_403_FORBIDDEN) + + # 处理订单相关操作 + if operation == 'cancel_refund': + return self._cancel_refund(dingdan, request.data.get('liyou', '')) + elif operation == 'modify_refund_reason': + return self._modify_refund_reason(dingdan, request.data.get('liyou', '')) + + # 处理罚款相关操作 + 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) + except Fadan.DoesNotExist: + return Response({'code': 404, 'msg': '罚单不存在'}, status=status.HTTP_404_NOT_FOUND) + if fadan.guanliandingdan_id != dingdan_id: + return Response({'code': 403, 'msg': '罚单与订单不匹配'}, status=status.HTTP_403_FORBIDDEN) + + if operation == 'modify_penalty': + return self._modify_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine')) + elif operation == 'cancel_penalty': + return self._cancel_penalty(fadan, request.data.get('liyou', '')) + elif operation == 'resubmit_penalty': + return self._resubmit_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine')) + else: + return Response({'code': 400, 'msg': '无效的操作类型'}, status=status.HTTP_400_BAD_REQUEST) + + def _cancel_refund(self, dingdan, liyou): + """撤销退款申请(状态4 → 状态8,清空退款理由)""" + if dingdan.zhuangtai != 4: + return Response({'code': 400, 'msg': '只有退款中的订单可以撤销退款申请'}) + with transaction.atomic(): + dingdan.zhuangtai = 8 + dingdan.tkly = '' # 清空退款理由 + dingdan.save() + # 记录撤销理由可存入日志(如需要可另建日志表,此处简单处理) + logger.info(f"商家撤销退款订单 {dingdan.dingdan_id},理由: {liyou}") + return Response({'code': 0, 'msg': '已撤销退款申请'}) + + def _modify_refund_reason(self, dingdan, liyou): + """修改退款理由(仅状态4时)""" + if dingdan.zhuangtai != 4: + return Response({'code': 400, 'msg': '只有退款中的订单可以修改退款理由'}) + if not liyou.strip(): + return Response({'code': 400, 'msg': '退款理由不能为空'}) + dingdan.tkly = liyou[:500] + dingdan.save() + return Response({'code': 0, 'msg': '退款理由修改成功'}) + + def _modify_penalty(self, fadan, liyou, jine_str): + """修改罚款(仅状态1待缴纳 或 3申诉中)""" + if fadan.zhuangtai not in (1, 3): + return Response({'code': 400, 'msg': '当前罚单状态不可修改'}) + if not liyou.strip(): + return Response({'code': 400, 'msg': '罚款理由不能为空'}) + try: + jine = Decimal(jine_str) + if jine <= 0: + raise ValueError + except: + return Response({'code': 400, 'msg': '罚款金额必须为正数'}) + with transaction.atomic(): + fadan.chufaliyou = liyou[:500] + fadan.fakuanjine = jine + # 如果之前是驳回状态修改,需要重置状态为待缴纳?不,修改不应改变状态 + fadan.save() + return Response({'code': 0, 'msg': '罚款修改成功'}) + + def _cancel_penalty(self, fadan, liyou): + """撤销罚款(状态1或3 → 状态4 已驳回)""" + if fadan.zhuangtai not in (1, 3): + return Response({'code': 400, 'msg': '当前罚单状态不可撤销'}) + if not liyou.strip(): + return Response({'code': 400, 'msg': '驳回理由不能为空'}) + with transaction.atomic(): + fadan.zhuangtai = 4 # 已驳回 + fadan.bohuiliyou = liyou[:500] + fadan.save() + return Response({'code': 0, 'msg': '罚款已撤销(驳回)'}) + + def _resubmit_penalty(self, fadan, liyou, jine_str): + """再次申请罚款(状态4 → 状态1,修改理由和金额)""" + if fadan.zhuangtai != 4: + return Response({'code': 400, 'msg': '只有已驳回的罚单可再次申请'}) + if not liyou.strip(): + return Response({'code': 400, 'msg': '罚款理由不能为空'}) + try: + jine = Decimal(jine_str) + if jine <= 0: + raise ValueError + except: + return Response({'code': 400, 'msg': '罚款金额必须为正数'}) + with transaction.atomic(): + fadan.zhuangtai = 1 # 待缴纳 + fadan.chufaliyou = liyou[:500] + fadan.fakuanjine = jine + fadan.bohuiliyou = '' # 清空驳回理由 + fadan.save() return Response({'code': 0, 'msg': '已重新申请罚款'}) \ No newline at end of file diff --git a/houtai/view.py b/houtai/view.py index e4bd717..f902651 100644 --- a/houtai/view.py +++ b/houtai/view.py @@ -1,3 +1,4 @@ +import hmac import threading import traceback from calendar import monthrange @@ -1234,6 +1235,9 @@ class PartnerGetOrderDataView(APIView): 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() @@ -1339,6 +1343,9 @@ class PartnerRefundNotifyView(APIView): 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() @@ -2093,6 +2100,9 @@ class PartnerTransferHallNotifyView(APIView): 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() @@ -2618,6 +2628,9 @@ class PartnerUpdateSelfConfigView(APIView): 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', {}) @@ -3174,6 +3187,9 @@ class PartnerPunishNotifyView(APIView): 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() @@ -9722,6 +9738,9 @@ class PartnerFineNotifyView(APIView): 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() @@ -12453,8 +12472,8 @@ class KptxwztjbView(APIView): return Response({'code': 400, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) # 2. 验证同步令牌 - SYNC_TOKEN = getattr(settings, 'CROSS_PLATFORM_SYNC_TOKEN', 'xK9mP2vL8qW5rT7y') - if token != SYNC_TOKEN: + 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) diff --git a/requirements.txt b/requirements.txt index 0563dae..f1573cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ click-repl==0.3.0 cos_python_sdk_v5==1.9.40 crcmod==1.7 cron_descriptor==2.0.6 +defusedxml Django==4.2.27 django-celery-beat==2.5.0 django-celery-results==2.5.0 diff --git a/shangpin/views.py b/shangpin/views.py index fa8f3f9..60d3512 100644 --- a/shangpin/views.py +++ b/shangpin/views.py @@ -465,7 +465,7 @@ import random import string import time import hashlib -import xml.etree.ElementTree as ET +import defusedxml.ElementTree as ET import requests from django.conf import settings from rest_framework.views import APIView @@ -731,7 +731,7 @@ from django.views import View from django.http import HttpResponse from yonghu.models import UserDashou, UserMain from shangpin.models import Czjilu -import xml.etree.ElementTree as ET +import defusedxml.ElementTree as ET import hashlib from django.conf import settings import logging @@ -1686,7 +1686,7 @@ import random import string import time import hashlib -import xml.etree.ElementTree as ET +import defusedxml.ElementTree as ET import requests from django.conf import settings import logging @@ -2387,7 +2387,7 @@ class HuiyuanHuitiao(View): from django.http import HttpResponse from yonghu.models import UserDashou, UserMain, UserGuanshi from shangpin.models import Czjilu, Huiyuan, Huiyuangoumai, Gsfenhong -import xml.etree.ElementTree as ET +import defusedxml.ElementTree as ET import hashlib from django.conf import settings from django.utils import timezone @@ -3016,7 +3016,7 @@ import time import random import string import hashlib -import xml.etree.ElementTree as ET +import defusedxml.ElementTree as ET import requests from decimal import Decimal from django.conf import settings diff --git a/utils/exception_handler.py b/utils/exception_handler.py index 55f4576..81cdfb2 100644 --- a/utils/exception_handler.py +++ b/utils/exception_handler.py @@ -54,5 +54,26 @@ def custom_exception_handler(exc, context): return Response(standard_response, status=response.status_code) - # 对于非DRF异常,这里可以根据需要处理 + # 对于非DRF异常(response为None),生产环境返回通用错误信息 + if response is None: + from django.conf import settings + request = context.get('request') + user_info = '未知' + if request: + user_info = f"用户: {request.user.id if request.user.is_authenticated else '未登录'}" + logger.error( + f"未处理异常 - 路径: {request.path if request else '未知'}, " + f"方法: {request.method if request else '未知'}, {user_info}, " + f"错误类型: {type(exc).__name__}, 错误: {str(exc)}" + ) + if not settings.DEBUG: + return Response( + {'code': 500, 'message': '服务器内部错误,请稍后重试'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + return Response( + {'code': 500, 'message': str(exc)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + return response \ No newline at end of file diff --git a/utils/oss_utils.py b/utils/oss_utils.py index 662dc5d..95cbfb7 100644 --- a/utils/oss_utils.py +++ b/utils/oss_utils.py @@ -5,255 +5,6 @@ from django.conf import settings from PIL import Image, UnidentifiedImageError import io -'''try: - from qcloud_cos import CosConfig, CosS3Client - - TENCENT_COS_AVAILABLE = True -except ImportError: - TENCENT_COS_AVAILABLE = False - -try: - import oss2 - - ALIYUN_OSS_AVAILABLE = True -except ImportError: - ALIYUN_OSS_AVAILABLE = False - - -def validate_image(file): - """ - 验证图片文件 - 使用Pillow检查图片是否有效 - """ - try: - # 打开图片但不保存到磁盘 - img = Image.open(file) - img.verify() # 验证文件完整性 - - # 重置文件指针(因为PIL读取后会移动指针) - file.seek(0) - - # 检查图片格式 - allowed_formats = ['JPEG', 'PNG', 'GIF', 'WEBP', 'BMP'] - if img.format not in allowed_formats: - return False, f"不支持{img.format}格式,请使用JPEG、PNG、GIF、WEBP或BMP格式" - - # 检查图片大小(最大5MB) - file_size = len(file.read()) - file.seek(0) - if file_size > 5 * 1024 * 1024: - return False, "图片大小不能超过5MB" - - # 检查图片尺寸(最大2000x2000) - img = Image.open(file) - file.seek(0) - if img.width > 3000 or img.height > 3000: - return False, "图片尺寸不能超过2000x2000像素" - - return True, "图片验证通过" - - except UnidentifiedImageError: - return False, "文件不是有效的图片" - except Exception as e: - return False, f"图片验证失败: {str(e)}" - - -def get_oss_client(): - """ - 获取OSS客户端 - 根据配置自动选择云厂商 - """ - oss_type = getattr(settings, 'OSS_TYPE', 'tencent') # 默认为腾讯云 - - if oss_type == 'tencent' and TENCENT_COS_AVAILABLE: - # 腾讯云COS - config = CosConfig( - Region=getattr(settings, 'COS_REGION', 'ap-shanghai'), - SecretId=getattr(settings, 'COS_SECRET_ID', ''), - SecretKey=getattr(settings, 'COS_SECRET_KEY', ''), - Scheme=getattr(settings, 'COS_SCHEME', 'https') - ) - return CosS3Client(config) - - elif oss_type == 'aliyun' and ALIYUN_OSS_AVAILABLE: - # 阿里云OSS - auth = oss2.Auth( - getattr(settings, 'ALIYUN_ACCESS_KEY_ID', ''), - getattr(settings, 'ALIYUN_ACCESS_KEY_SECRET', '') - ) - endpoint = getattr(settings, 'ALIYUN_OSS_ENDPOINT', '') - bucket_name = getattr(settings, 'ALIYUN_OSS_BUCKET', '') - return oss2.Bucket(auth, endpoint, bucket_name) - - else: - raise ImportError(f"不支持{oss_type}云存储或相关SDK未安装") - - -def upload_to_oss(file_obj, file_path): - """ - 通用上传文件到OSS - file_obj: Django的UploadedFile对象或类文件对象 - file_path: 在OSS中的相对路径,如 'avatar/123/abc.jpg' - 返回: 文件访问URL - """ - try: - oss_type = getattr(settings, 'OSS_TYPE', 'tencent') - - if oss_type == 'tencent': - return _upload_to_tencent_cos(file_obj, file_path) - elif oss_type == 'aliyun': - return _upload_to_aliyun_oss(file_obj, file_path) - else: - raise ValueError(f"不支持的OSS类型: {oss_type}") - - except Exception as e: - print(f"OSS上传失败: {e}") - return None - - -def _upload_to_tencent_cos(file_obj, file_path): - """ - 上传到腾讯云COS - """ - client = get_oss_client() - - # 获取文件扩展名 - ext = file_path.split('.')[-1].lower() if '.' in file_path else '' - content_type_map = { - 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', - 'png': 'image/png', 'gif': 'image/gif', - 'webp': 'image/webp', 'bmp': 'image/bmp' - } - content_type = content_type_map.get(ext, 'image/jpeg') - - # 如果是Django的UploadedFile,需要读取内容 - if hasattr(file_obj, 'read'): - file_content = file_obj.read() - else: - file_content = file_obj - - # 上传文件 - response = client.put_object( - Bucket=getattr(settings, 'COS_BUCKET', ''), - Body=file_content, - Key=file_path, - ContentType=content_type, - ContentDisposition='inline' - ) - - # 返回完整URL - domain = getattr(settings, 'COS_DOMAIN', '') - return f"{domain}/{file_path}" - - -def _upload_to_aliyun_oss(file_obj, file_path): - """ - 上传到阿里云OSS - """ - bucket = get_oss_client() - - # 如果是Django的UploadedFile,需要读取内容 - if hasattr(file_obj, 'read'): - file_content = file_obj.read() - else: - file_content = file_obj - - # 上传文件 - result = bucket.put_object(file_path, file_content) - - if result.status == 200: - # 阿里云OSS返回的URL - domain = getattr(settings, 'ALIYUN_OSS_DOMAIN', '') - return f"{domain}/{file_path}" - else: - return None - - -def delete_from_oss(file_url): - """ - 从OSS删除文件 - file_url: 可以是完整URL或相对路径 - """ - try: - if not file_url: - return False - - oss_type = getattr(settings, 'OSS_TYPE', 'tencent') - - # 从URL中提取文件路径 - if oss_type == 'tencent': - domain = getattr(settings, 'COS_DOMAIN', '') - - # 判断是完整URL还是相对路径 - if file_url.startswith('http'): - # 完整URL - if domain not in file_url: - return False # URL不属于我们的域名 - file_key = file_url.replace(f"{domain}/", "") - else: - # 相对路径,直接使用 - file_key = file_url - - return _delete_from_tencent_cos(file_key) - - elif oss_type == 'aliyun': - domain = getattr(settings, 'ALIYUN_OSS_DOMAIN', '') - - if file_url.startswith('http'): - if domain not in file_url: - return False - file_key = file_url.replace(f"{domain}/", "") - else: - file_key = file_url - - return _delete_from_aliyun_oss(file_key) - - else: - return False - - except Exception as e: - print(f"OSS删除失败: {e}") - return False - - -def _delete_from_tencent_cos(file_key): - """ - 从腾讯云COS删除文件 - """ - client = get_oss_client() - - try: - client.delete_object( - Bucket=getattr(settings, 'COS_BUCKET', ''), - Key=file_key - ) - return True - except Exception as e: - print(f"腾讯云COS删除失败: {e}") - return False - - -def _delete_from_aliyun_oss(file_key): - """ - 从阿里云OSS删除文件 - """ - bucket = get_oss_client() - - try: - bucket.delete_object(file_key) - return True - except Exception as e: - print(f"阿里云OSS删除失败: {e}") - return False''' - - - -# utils/oss_utils.py -import os -import uuid -from django.conf import settings -from PIL import Image, UnidentifiedImageError -import io try: from qcloud_cos import CosConfig, CosS3Client @@ -508,4 +259,4 @@ def _delete_from_aliyun_oss(file_key): return True except Exception as e: print(f"阿里云OSS删除失败: {e}") - return False \ No newline at end of file + return False diff --git a/utils/redis_lock.py b/utils/redis_lock.py index c46d078..44b168c 100644 --- a/utils/redis_lock.py +++ b/utils/redis_lock.py @@ -1,18 +1,57 @@ -import redis +import uuid +import logging from django.conf import settings +from django.core.cache import cache + +logger = logging.getLogger(__name__) + +# Redis客户端(延迟初始化) +_redis_client = None + +def _get_redis_client(): + global _redis_client + if _redis_client is None: + import redis + _redis_client = redis.Redis( + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + password=settings.REDIS_PASSWORD, + db=settings.REDIS_DB_CELERY, + decode_responses=True, + ) + return _redis_client -redis_client = redis.Redis( - host=settings.REDIS_HOST, - port=settings.REDIS_PORT, - password=settings.REDIS_PASSWORD, - db=2, - decode_responses=True -) def acquire_lock(lock_key, timeout=10): - """获取锁,返回是否成功""" - return redis_client.set(lock_key, '1', nx=True, ex=timeout) + """获取分布式锁,返回锁标识符,失败返回None""" + client = _get_redis_client() + identifier = str(uuid.uuid4()) + result = client.set(lock_key, identifier, nx=True, ex=timeout) + if result: + logger.debug(f"获取锁成功: {lock_key}, 标识: {identifier}") + return identifier + return None -def release_lock(lock_key): - """释放锁""" - redis_client.delete(lock_key) \ No newline at end of file + +def release_lock(lock_key, identifier): + """释放分布式锁(仅当标识符匹配时才释放)""" + if identifier is None: + return False + client = _get_redis_client() + lua_script = """ + if redis.call('get', KEYS[1]) == ARGV[1] then + return redis.call('del', KEYS[1]) + else + return 0 + end + """ + try: + result = client.eval(lua_script, 1, lock_key, identifier) + if result: + logger.debug(f"释放锁成功: {lock_key}") + else: + logger.warning(f"释放锁失败(标识不匹配或锁已过期): {lock_key}") + return bool(result) + except Exception as e: + logger.error(f"释放锁异常: {lock_key}, 错误: {e}") + return False diff --git a/utils/wechat_v3.py b/utils/wechat_v3.py index 4fa097c..0920856 100644 --- a/utils/wechat_v3.py +++ b/utils/wechat_v3.py @@ -1,112 +1,4 @@ -# utils/wechat_v3.py -'''import time -import hashlib -import base64 -import json -from urllib.parse import urlparse - -import rsa -from Crypto.Cipher import AES - -from django.conf import settings - - -def load_private_key(): - """加载商户私钥(PEM格式)""" - with open(settings.WECHAT_PAY_V3_CONFIG['PRIVATE_KEY_PATH'], 'rb') as f: - key_data = f.read() - return rsa.PrivateKey.load_pkcs1(key_data) - - -def build_authorization(method, url, body): - """ - 生成V3接口的Authorization头 - :param method: GET/POST - :param url: 完整URL(含域名和路径) - :param body: 请求体JSON字符串 - :return: Authorization字符串 - """ - private_key = load_private_key() - mchid = settings.WECHAT_PAY_V3_CONFIG['MCHID'] - serial_no = settings.WECHAT_PAY_V3_CONFIG['CERT_SERIAL_NO'] - - # 提取URL路径(不含query) - parsed = urlparse(url) - path = parsed.path - if parsed.query: - path += '?' + parsed.query - - timestamp = str(int(time.time())) - nonce = hashlib.md5((timestamp + 'wechat').encode()).hexdigest()[:32] - - # 构造签名串 - message = '\n'.join([method, path, timestamp, nonce, body]) + '\n' - - # 使用私钥签名(SHA256) - signature = rsa.sign(message.encode('utf-8'), private_key, 'SHA-256') - signature_b64 = base64.b64encode(signature).decode('utf-8') - - # 组装Authorization - auth = f'WECHATPAY2-SHA256-RSA2048 mchid="{mchid}",nonce_str="{nonce}",timestamp="{timestamp}",serial_no="{serial_no}",signature="{signature_b64}"' - return auth - - -def verify_wechat_sign(headers, body): - """ - 验证微信回调的签名 - 需要提前从微信平台下载证书公钥,存放在 PLATFORM_CERT_DIR 下,文件名 = 序列号.pem - :param headers: 请求头 - :param body: 原始请求体字符串 - :return: bool - """ - serial = headers.get('Wechatpay-Serial') - signature = headers.get('Wechatpay-Signature') - timestamp = headers.get('Wechatpay-Timestamp') - nonce = headers.get('Wechatpay-Nonce') - - if not all([serial, signature, timestamp, nonce]): - return False - - # 构造验签名串 - message = '\n'.join([timestamp, nonce, body]) + '\n' - - # 读取对应序列号的平台证书公钥 - cert_path = f"{settings.WECHAT_PAY_V3_CONFIG['PLATFORM_CERT_DIR']}/{serial}.pem" - try: - with open(cert_path, 'rb') as f: - pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(f.read()) - except FileNotFoundError: - # 如果证书不存在,可尝试下载(生产环境建议定时下载) - return False - - signature_bytes = base64.b64decode(signature) - try: - rsa.verify(message.encode('utf-8'), signature_bytes, pub_key) - return True - except rsa.VerificationError: - return False - - -def decrypt_callback_ciphertext(associated_data, nonce, ciphertext): - """ - 使用APIv3密钥解密回调中的加密数据 - :param associated_data: 附加数据 - :param nonce: 随机串 - :param ciphertext: 密文(Base64) - :return: 解密后的JSON字符串 - """ - api_v3_key = settings.WECHAT_PAY_V3_CONFIG['API_V3_KEY'].encode('utf-8') - nonce_bytes = nonce.encode('utf-8') - ciphertext_bytes = base64.b64decode(ciphertext) - - # AES-GCM解密 - cipher = AES.new(api_v3_key, AES.MODE_GCM, nonce=nonce_bytes) - if associated_data: - cipher.update(associated_data.encode('utf-8')) - plaintext = cipher.decrypt_and_verify(ciphertext_bytes[:-16], ciphertext_bytes[-16:]) - return plaintext.decode('utf-8')''' - -# utils/wechat_v3.py +import re import time import hashlib import base64 @@ -178,6 +70,16 @@ def verify_wechat_sign(headers, body): if not all([serial, signature, timestamp, nonce]): return False + if not re.match(r'^[A-Za-z0-9-]+$', serial): + return False + + try: + ts = int(headers.get('Wechatpay-Timestamp', '0')) + if abs(time.time() - ts) > 300: # 5分钟有效期 + return False + except (ValueError, TypeError): + return False + message = '\n'.join([timestamp, nonce, body]) + '\n' cert_path = f"{settings.WECHAT_PAY_V3_CONFIG['PLATFORM_CERT_DIR']}/{serial}.pem" if not os.path.exists(cert_path): @@ -204,4 +106,4 @@ def decrypt_callback_ciphertext(associated_data, nonce, ciphertext): if associated_data: cipher.update(associated_data.encode('utf-8')) plaintext = cipher.decrypt_and_verify(ciphertext_bytes[:-16], ciphertext_bytes[-16:]) - return plaintext.decode('utf-8') \ No newline at end of file + return plaintext.decode('utf-8') diff --git a/yonghu/tixian_shenhe_views.py b/yonghu/tixian_shenhe_views.py index 0886021..6e1b8a1 100644 --- a/yonghu/tixian_shenhe_views.py +++ b/yonghu/tixian_shenhe_views.py @@ -141,7 +141,8 @@ class TixianZddkshApplyView(APIView): user_main = request.user lock_key = f'tixian_audit_apply:{user_main.yonghuid}' - if not acquire_lock(lock_key, timeout=10): + identifier = acquire_lock(lock_key, timeout=10) + if not identifier: return Response({'code': 429, 'msg': '操作频繁,请稍后重试'}) try: @@ -172,7 +173,7 @@ class TixianZddkshApplyView(APIView): return Response({'code': 0, 'msg': '提现申请已提交,请等待审核', 'data': data}) finally: - release_lock(lock_key) + release_lock(lock_key, identifier) def process_audit_collect(request): @@ -233,7 +234,8 @@ def process_audit_collect(request): return quota_resp lock_key = f'tixian_collect:{shenhe_danhao}' - if not acquire_lock(lock_key, timeout=15): + identifier = acquire_lock(lock_key, timeout=15) + if not identifier: return Response({'code': 429, 'msg': '操作频繁,请稍后重试'}) tixian_id = None @@ -372,7 +374,7 @@ def process_audit_collect(request): }) finally: - release_lock(lock_key) + release_lock(lock_key, identifier) except Exception as e: logger.error( diff --git a/yonghu/views.py b/yonghu/views.py index d7e09b3..b626da0 100644 --- a/yonghu/views.py +++ b/yonghu/views.py @@ -375,7 +375,7 @@ import logging from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse from django.utils import timezone -from xml.etree import ElementTree as ET +import defusedxml.ElementTree as ET from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status @@ -526,122 +526,6 @@ class WeixinOfficialCallbackView(APIView): -'''class WeixinOfficialCallbackView(APIView): - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - """ - 微信服务号回调接口 - 处理用户关注、取消关注等事件 - """ - - 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"收到服务号事件: {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) - - return HttpResponse('success') - - except Exception as e: - logger.error(f"处理服务号事件异常: {str(e)}") - 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 - - if unionid: - # 保存或更新关联 - OfficialAccountUser.objects.update_or_create( - unionid=unionid, - defaults={ - 'official_openid': from_user, - 'is_subscribed': True - } - ) - logger.info(f"用户关注成功: unionid={unionid}") - else: - logger.warning(f"用户关注但未获取到UnionID") - - # 返回欢迎消息 - return self.generate_text_response(root, "感谢关注!您将收到订单通知。") - - except Exception as e: - logger.error(f"处理关注事件异常: {str(e)}") - return HttpResponse('success') - - def handle_unsubscribe_event(self, root, from_user): - """处理用户取消关注事件""" - try: - # 获取UnionID - unionid_elem = root.find('UnionId') - unionid = unionid_elem.text if unionid_elem is not None else None - - if unionid: - OfficialAccountUser.objects.filter(unionid=unionid).update(is_subscribed=False) - logger.info(f"用户取消关注: unionid={unionid}") - else: - OfficialAccountUser.objects.filter(official_openid=from_user).update(is_subscribed=False) - - return HttpResponse('success') - - except Exception as e: - logger.error(f"处理取消关注事件异常: {str(e)}") - return HttpResponse('success') - - def generate_text_response(self, root, content): - """生成文本消息响应""" - 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')''' @@ -814,259 +698,6 @@ class UserInfoUpdateView(APIView): -'''class UserInfoUpdateView(APIView): - """ - 用户个人信息修改接口 - - 功能: - - 上传 / 更新头像 - - 修改昵称 - - 更新手机号(支持明文 phone 或微信 getPhoneNumber 组件返回的 shoujihao_code) - 当传 shoujihao_code 时,会通过微信接口解密得到真实手机号并自动设置 shoujihao_renzheng=True - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - """ - 处理 POST 请求,支持 multipart/form-data 格式 - """ - try: - # ---------- 获取当前用户 ---------- - user_main = request.user - - # ---------- 获取或创建老板扩展表记录 ---------- - try: - user_boss = UserBoss.objects.get(user=user_main) - except UserBoss.DoesNotExist: - user_boss = UserBoss.objects.create(user=user_main, nickname='微信用户') - - response_data = {} - - # ==================== 1. 处理头像上传 ==================== - avatar_file = request.FILES.get('avatar') - if avatar_file: - # 验证图片格式 / 大小 - is_valid, error_msg = validate_image(avatar_file) - if not is_valid: - return Response({ - 'code': 1, - 'msg': f'图片验证失败: {error_msg}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 上传头像到 OSS / COS - avatar_url = self.handle_avatar_upload(user_main, avatar_file) - if avatar_url: - response_data['touxiang'] = avatar_url - user_main.avatar = avatar_url - else: - return Response({ - 'code': 2, - 'msg': '头像上传失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # ==================== 2. 处理昵称更新 ==================== - nicheng = request.data.get('nicheng') - if nicheng is not None: - nicheng = str(nicheng).strip() - if nicheng: - if len(nicheng) > 20: - return Response({ - 'code': 3, - 'msg': '昵称不能超过20个字符', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - user_boss.nickname = nicheng - response_data['nicheng'] = nicheng - - # ==================== 3. 处理手机号更新 ==================== - # 明文手机号(传统方式) - phone =None - shoujihao_code = request.data.get('shoujihao_code') # 微信手机号快速验证组件返回的临时 code - - if shoujihao_code: - # 如果前端传了 shoujihao_code,则通过微信接口解密得到真实手机号 - try: - phone = self._get_phone_by_code(shoujihao_code) - logger.info(f"手机号解密成功: {phone}") - except Exception as e: - logger.error(f"手机号解密失败: {str(e)}") - return Response({ - 'code': 6, - 'msg': f'获取手机号失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - if phone is not None: - phone = str(phone).strip() - if phone: - # 验证手机号格式(11 位数字) - if not phone.isdigit() or len(phone) != 11: - return Response({ - 'code': 4, - 'msg': '请输入11位有效的手机号码', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 更新主表手机号,并标记手机号已认证 - user_main.phone = phone - user_main.shoujihao_renzheng = True - response_data['phone'] = phone - - # ==================== 4. 检查是否有任何更新 ==================== - if not response_data: - return Response({ - 'code': 5, - 'msg': '没有需要更新的数据', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # ==================== 5. 事务保存 ==================== - with transaction.atomic(): - user_main.save() - user_boss.save() - - # ==================== 6. 返回成功 ==================== - return Response({ - 'code': 0, - 'msg': '更新成功', - 'data': response_data - }) - - except Exception as e: - logger.exception("用户信息更新异常") - return Response({ - 'code': 99, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # ==================== 原有方法:头像上传 ==================== - def handle_avatar_upload(self, user_main, avatar_file): - """ - 上传头像到 OSS / COS,并返回存储的相对路径 - """ - try: - yonghuid = user_main.yonghuid - if not yonghuid: - logger.warning("用户 ID 为空,无法上传头像") - return None - - # 获取旧头像,用于后续删除 - old_avatar_url = user_main.avatar - - # 生成新文件名 - file_extension = os.path.splitext(avatar_file.name)[1].lower() - if not file_extension: - file_extension = '.jpg' - unique_filename = f"{uuid.uuid4().hex}{file_extension}" - - # OSS 文件路径:avatar/{yonghuid}/{filename} - avatar_folder = 'avatar' - oss_file_path = f"{avatar_folder}/{yonghuid}/{unique_filename}" - - # 删除旧头像(如果存在) - if old_avatar_url and old_avatar_url.strip(): - delete_from_oss(old_avatar_url) - - # 上传新头像 - new_avatar_url = upload_to_oss(avatar_file, oss_file_path) - if new_avatar_url: - logger.info(f"头像上传成功: {oss_file_path}") - return oss_file_path - else: - logger.error("头像上传失败,upload_to_oss 返回 None") - return None - - except Exception as e: - logger.exception("处理头像上传失败") - return None - - # ==================== 新增方法:微信接口相关 ==================== - - def _get_access_token(self): - """ - 获取微信小程序全局 access_token,带缓存 - access_token 有效期 7200 秒,提前 300 秒过期 - """ - import requests - from django.core.cache import cache - - # 尝试从缓存中读取 - token = cache.get('wechat_access_token') - if token: - return token - - # 从 settings 读取 AppID 和 AppSecret - appid = getattr(settings, 'WEIXIN_APPID', None) - secret = getattr(settings, 'WEIXIN_SECRET', None) - if not appid or not secret: - raise ValueError( - 'settings.py 中缺少 WECHAT_APPID 或 WECHAT_APPSECRET,' - '请添加:\n' - "WECHAT_APPID = 'wx0e4be86faac4a8d1'\n" - "WECHAT_APPSECRET = '你的小程序密钥'" - ) - - url = 'https://api.weixin.qq.com/cgi-bin/token' - params = { - 'grant_type': 'client_credential', - 'appid': appid, - 'secret': secret - } - try: - resp = requests.get(url, params=params) - data = resp.json() - except Exception as e: - raise Exception(f'请求微信 access_token 接口异常: {str(e)}') - - if 'access_token' in data: - token = data['access_token'] - # 缓存时间设 7100 秒,提前 5 分钟过期 - cache.set('wechat_access_token', token, timeout=7100) - logger.info("成功获取微信 access_token") - return token - else: - errcode = data.get('errcode') - errmsg = data.get('errmsg') - logger.error(f"获取 access_token 失败: errcode={errcode}, errmsg={errmsg}") - raise Exception(f"获取 access_token 失败: {errmsg}") - - def _get_phone_by_code(self, code): - """ - 通过微信手机号快速验证组件返回的 code 换取真实手机号 - 接口文档:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html - """ - import requests - - # 先获取有效的 access_token - access_token = self._get_access_token() - - # 构建请求 URL - url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={access_token}' - - # 请求体:code 必须放在 JSON 中 - payload = {'code': code} - - try: - resp = requests.post(url, json=payload, timeout=10) - data = resp.json() - except Exception as e: - raise Exception(f'请求微信手机号解密接口异常: {str(e)}') - - errcode = data.get('errcode') - if errcode == 0: - phone_info = data.get('phone_info', {}) - pure_phone = phone_info.get('purePhoneNumber') - if not pure_phone: - raise Exception('手机号为空,可能用户未授权真实手机号') - return pure_phone - else: - errmsg = data.get('errmsg') - logger.error(f"手机号解密失败: errcode={errcode}, errmsg={errmsg}") - raise Exception(f'微信接口错误 (errcode={errcode}): {errmsg}')''' @@ -1522,43 +1153,8 @@ class ShangJiaXinXiView(APIView): -'''class ShangJiaXinXiView(APIView): - """ - 获取商家信息接口 (POST /yonghu/shangjiaxinxi) - 权限:需JWT认证 - """ - permission_classes = [IsAuthenticated] - def post(self, request): - try: - # 关键:使用 get() 方法。如果反向关联不存在,会抛出 UserShangjia.DoesNotExist 异常。 - shangjia = request.user.shop_profile - # 如果反向关系存在,但你想更显式地查询,也可以这样写(效果相同): - # shangjia = UserShangjia.objects.get(user=request.user) - except ObjectDoesNotExist: - # 捕获“不存在”异常,说明当前用户还没有商家扩展资料 - return Response({ - 'code': 4001, # 你可以定义特定的业务错误码 - 'msg': '您还未注册成为商家或商家资料不存在', - 'data': None - }, status=status.HTTP_200_OK) # 业务错误,HTTP状态码通常仍用200 - - # 构建返回给前端的数据 - response_data = { - 'sjzhzhuangtai': shangjia.zhuangtai, # 商家账号状态 - 'fadanzong': shangjia.fabu, # 发单总量 - 'tuikuanzong': shangjia.tuikuan, # 退款总量 - 'sjyue': float(shangjia.yue), # 商家余额 - 'riliushui': float(shangjia.jinriliushui), # 今日流水 - 'yueliushui': float(shangjia.jinyueliushui), # 今月流水 - } - - return Response({ - 'code': 200, - 'msg': '获取商家信息成功', - 'data': response_data - })''' @@ -1755,12 +1351,6 @@ class DashouZhuceView(APIView): }, status=status.HTTP_404_NOT_FOUND) # 4. 验证管事状态 - '''if guanshi_profile.zhuangtai != 1: - return Response({ - 'code': 403, - 'message': '该邀请码对应的管事账号已被禁用', - 'data': None - }, status=status.HTTP_403_FORBIDDEN)''' # 5. 核心:在数据库事务中创建用户的所有身份 try: @@ -1778,8 +1368,6 @@ class DashouZhuceView(APIView): ) # 5.2 创建商家扩展表 - # 尝试从老板表获取昵称,没有则用默认 - '''boss_nickname = '普通商家' try: boss_profile = current_user.boss_profile if boss_profile.nickname: @@ -1787,23 +1375,8 @@ class DashouZhuceView(APIView): except: pass - # 商家昵称需要唯一,这里用 用户ID + “的店铺” 来确保唯一性 - shangjia_nicheng = f"{current_user.yonghuid}的店铺" - shop_profile = UserShangjia.objects.create( - user=current_user, - nicheng=shangjia_nicheng, - # 其他字段使用默认值 - )''' # 5.3 创建管事扩展表 - # 生成管事的邀请码 - '''guanshi_yaoqingma = chuangjianYaoqingma(str(current_user.yonghuid)) - # 如果生成失败,guanshi_yaoqingma可能为None或空,根据你的算法调整 - new_guanshi_profile = UserGuanshi.objects.create( - user=current_user, - yaoqingma=guanshi_yaoqingma, - # 其他字段使用默认值 - )''' # 5.4 更新主表的用户类型 (如果需要,可以设为'dashou',或保持原逻辑) # current_user.user_type = 'dashou' # 根据你的业务逻辑决定 @@ -2091,42 +1664,7 @@ class TixianXinxiHuoquView(APIView): 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -'''class TixianXinxiHuoquView(APIView): - """ - 获取用户提现收款信息接口 - 返回用户已设置的手机号、收款码、收款账号 - """ - permission_classes = [permissions.IsAuthenticated] - def post(self, request): - """ - 处理获取收款信息的POST请求 - 直接返回用户当前的收款设置 - """ - try: - # 从request.user获取当前用户 - user_main = request.user - - # 构建返回数据 - 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 '' - } - - # 返回成功响应 - 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 @@ -2297,559 +1835,7 @@ import traceback from .models import UserMain, UserDashou, UserGuanshi, Tixianjilu -'''class TixianShenqingView(APIView): - """ - 用户提现申请接口(修复版) - 支持打手佣金提现、管事分红提现、组长分红提现 - 严格校验所有业务条件,修复400错误问题 - """ - permission_classes = [permissions.IsAuthenticated] - def post(self, request): - """ - 处理提现申请的POST请求 - 加强参数验证和错误处理 - """ - try: - # 获取当前用户 - user_main = request.user - yonghuid = user_main.yonghuid - - # 1. 获取并严格验证前端参数 - leixing = request.data.get('leixing') # 提现类型:1-佣金 2-分红 3-组长分红 - jine = request.data.get('jine') - fangshi = request.data.get('fangshi') - txskfs = request.data.get('txskfs', 0) - - # ==================== 【新增】罚单检查 ==================== - from utils.fadan_utils import check_fadan_qiangdan_eligible - fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(request.user.yonghuid, 2) - if not fadan_ok: - return Response({'code': 400, 'msg': fadan_msg}) - # ========================================================= - - # 验证必需参数是否存在 - missing_params = [] - if leixing is None: - missing_params.append('leixing') - if jine is None: - missing_params.append('jine') - if fangshi is None: - missing_params.append('fangshi') - - if missing_params: - return Response({ - 'code': 1, - 'msg': f'缺少必需参数: {", ".join(missing_params)}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证参数类型和格式 - try: - leixing = int(leixing) - if isinstance(jine, str): - jine = decimal.Decimal(jine) - else: - 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)}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证提现金额必须大于0 - if jine <= decimal.Decimal('0'): - return Response({ - 'code': 3, - 'msg': '提现金额必须大于0', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证提现类型有效性(新增类型3) - if leixing not in [1, 2, 3,4]: - return Response({ - 'code': 4, - 'msg': '无效的提现类型,只能是1(佣金)、2(分红)或3(组长分红)', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证收款方式有效性 - if fangshi not in [1, 2]: - return Response({ - 'code': 5, - 'msg': '无效的收款方式,只能是1(微信)或2(支付宝)', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 🔥【组长支持】获取对应费率(增加组长) - current_rate = decimal.Decimal('0.00') - if leixing == 1: - dashou_rate_obj = Lilubiao.objects.filter(fadanpingtai='5').first() - if dashou_rate_obj: - current_rate = decimal.Decimal(str(dashou_rate_obj.lilu)) - elif leixing == 2: - guanshi_rate_obj = Lilubiao.objects.filter(fadanpingtai='6').first() - if guanshi_rate_obj: - current_rate = decimal.Decimal(str(guanshi_rate_obj.lilu)) - elif leixing == 3: - # 组长提现费率 (fadanpingtai='8') - zuzhang_rate_obj = Lilubiao.objects.filter(fadanpingtai='8').first() - if zuzhang_rate_obj: - current_rate = decimal.Decimal(str(zuzhang_rate_obj.lilu)) - else: - return Response({ - 'code': 31, - 'msg': '组长费率未配置,无法提现', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - elif leixing == 4: - # 审核官提现费率 (fadanpingtai='9',数字可自定义) - kaoheguan_rate_obj = Lilubiao.objects.filter(fadanpingtai='9').first() - if kaoheguan_rate_obj: - current_rate = decimal.Decimal(str(kaoheguan_rate_obj.lilu)) - else: - return Response({ - 'code': 41, - 'msg': '审核官费率未配置,无法提现', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 计算手续费和实际到账金额 - shouxufei = jine * current_rate - shijidaozhang = jine - shouxufei - - # 验证手续费和实际到账金额的有效性 - if shijidaozhang <= decimal.Decimal('0'): - return Response({ - 'code': 6, - 'msg': '提现金额过低,扣除手续费后无实际到账', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 根据提现类型进行不同的校验 - if leixing == 1: - # 佣金提现 - 打手 - result = self.yanzheng_dashou_tixian(user_main, jine) - if result['code'] != 0: - return Response(result, status=status.HTTP_400_BAD_REQUEST) - - dashou_info = result['data'] - nicheng = dashou_info.get('nicheng', '') - - # 使用事务更新打手余额 - try: - with transaction.atomic(): - dashou_profile = UserDashou.objects.select_for_update().get(user=user_main) - if dashou_profile.yue < jine: - return Response({ - 'code': 13, - 'msg': '余额不足,无法提现', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - dashou_profile.yue = F('yue') - jine - dashou_profile.save() - dashou_profile.refresh_from_db() - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'更新余额失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - elif leixing == 2: - # 分红提现 - 管事 - result = self.yanzheng_guanshi_tixian(user_main, jine) - if result['code'] != 0: - return Response(result, status=status.HTTP_400_BAD_REQUEST) - - nicheng = "管事用户" - - try: - with transaction.atomic(): - guanshi_profile = UserGuanshi.objects.select_for_update().get(user=user_main) - if guanshi_profile.yue < jine: - return Response({ - 'code': 22, - 'msg': '余额不足,无法提现', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - guanshi_profile.yue = F('yue') - jine - guanshi_profile.save() - guanshi_profile.refresh_from_db() - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'更新余额失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 🔥【组长支持】新增组长提现分支 - elif leixing == 3: - # 组长分红提现 - result = self.yanzheng_zuzhang_tixian(user_main, jine) - if result['code'] != 0: - return Response(result, status=status.HTTP_400_BAD_REQUEST) - - zuzhang_info = result['data'] - nicheng = zuzhang_info.get('nicheng', '') - - try: - with transaction.atomic(): - zuzhang_profile = UserZuzhang.objects.select_for_update().get(user=user_main) - if zuzhang_profile.ketixian_jine < jine: - return Response({ - 'code': 32, - 'msg': '组长可提现金额不足', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - # 扣除申请金额 - zuzhang_profile.ketixian_jine = F('ketixian_jine') - jine - zuzhang_profile.save() - zuzhang_profile.refresh_from_db() - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'更新组长余额失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - # 🔥【审核官支持】新增审核官提现分支 - elif leixing == 4: - # 审核官分佣提现 - result = self.yanzheng_kaoheguan_tixian(user_main, jine) - if result['code'] != 0: - return Response(result, status=status.HTTP_400_BAD_REQUEST) - - kaoheguan_info = result['data'] - nicheng = kaoheguan_info.get('nicheng', '') - - try: - with transaction.atomic(): - kaoheguan_profile = UserShenheguan.objects.select_for_update().get(user=user_main) - if kaoheguan_profile.yue < jine: - return Response({ - 'code': 42, - 'msg': '审核官可提现金额不足', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - # 扣除申请金额 - kaoheguan_profile.yue = F('yue') - jine - kaoheguan_profile.save() - kaoheguan_profile.refresh_from_db() - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'更新审核官余额失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - # 3. 创建提现记录 - try: - avatar = user_main.avatar if user_main.avatar else '' - phone = user_main.phone if user_main.phone else '' - zhifu = user_main.zhifu if user_main.zhifu else '' - skzhanghao = user_main.skzhanghao if user_main.skzhanghao else '' - - # 对于打手或组长,从扩展表获取昵称 - if leixing == 1: - try: - dashou_profile = UserDashou.objects.get(user=user_main) - nicheng = dashou_profile.nicheng if dashou_profile.nicheng else '' - except UserDashou.DoesNotExist: - nicheng = '' - elif leixing == 3: - # 组长昵称从组长扩展表获取(如果需要) - try: - zuzhang_profile = UserZuzhang.objects.get(user=user_main) - # 组长扩展表可能没有昵称字段,可以从UserMain获取 - nicheng = user_main.nicheng if hasattr(user_main, 'nicheng') else '' - except UserZuzhang.DoesNotExist: - nicheng = '' - - tixian_jilu = 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='', - ) - - # 4. 返回成功响应 - return Response({ - 'code': 0, - 'msg': '提现申请提交成功', - 'data': { - 'tixian_id': tixian_jilu.id, - 'leixing': leixing, - 'jine': str(jine.quantize(decimal.Decimal('0.01'))), - 'fangshi': fangshi, - 'zhuangtai': 1, - 'create_time': tixian_jilu.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)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - 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 yanzheng_kaoheguan_tixian(self, user_main, jine): - """ - 校验审核官分佣提现条件 - """ - try: - # 查询审核官扩展表 - try: - kaoheguan_profile = UserShenheguan.objects.get(user=user_main) - except UserShenheguan.DoesNotExist: - return { - 'code': 40, - 'msg': '您不是审核官身份,无法提现分佣', - 'data': None - } - - # 校验1: 审核官账号状态 (zhuangtai != 1) - if kaoheguan_profile.zhuangtai != 1: - return { - 'code': 41, - 'msg': '审核官账号已被封禁,无法提现', - 'data': None - } - - # 校验2: 可提现金额是否足够 (yue < jine) - if kaoheguan_profile.yue < jine: - return { - 'code': 42, - 'msg': f'审核官可提现金额不足,当前可提: {kaoheguan_profile.yue},申请金额: {jine}', - 'data': None - } - - # 所有校验通过 - return { - 'code': 0, - 'msg': '校验通过', - 'data': { - 'nicheng': '审核官', # 可根据需要调整 - 'yue': kaoheguan_profile.yue - } - } - - except Exception as e: - return { - 'code': 99, - 'msg': f'审核官提现校验过程出错: {str(e)}', - 'data': None - } - - def yanzheng_dashou_tixian(self, user_main, jine): - """ - 校验打手佣金提现条件(修复版) - 修正了状态判断逻辑 - """ - try: - # print(f"开始校验打手提现 - 用户: {user_main.yonghuid}, 金额: {jine}") - - # 查询打手扩展表 - try: - dashou_profile = UserDashou.objects.get(user=user_main) - # print( - # f"打手信息 - 状态: {dashou_profile.zhuangtai}, 账号状态: {dashou_profile.zhanghaozhuangtai}, 积分: {dashou_profile.jifen}, 余额: {dashou_profile.yue}") - except UserDashou.DoesNotExist: - # print("打手扩展表记录不存在") - return { - 'code': 10, - 'msg': '您不是打手身份,无法提现佣金', - 'data': None - } - - # 校验1: 打手账号状态 (zhanghaozhuangtai != 1 则封禁) - if dashou_profile.zhanghaozhuangtai != 1: - return { - 'code': 11, - 'msg': '打手账号已被封禁,无法提现', - 'data': None - } - - # 校验2: 打手接单状态 (zhuangtai != 1 则有订单进行中) - 修复这里 - if dashou_profile.zhuangtai != 1: - return { - 'code': 12, - 'msg': '您有订单进行中,请完成后提现', - 'data': None - } - - # 校验3: 积分检查 (jifen != 10 则不足) - 新增 - if dashou_profile.jifen != 10: - return { - 'code': 15, # 使用新的错误码 - 'msg': '积分不足10分,请补充积分后提现', - 'data': None - } - - # 校验4: 余额是否足够 (yue < jine) - if dashou_profile.yue < jine: - return { - 'code': 13, - 'msg': f'余额不足,当前余额: {dashou_profile.yue},提现金额: {jine}', - 'data': None - } - - # print("打手提现校验通过") - - # 所有校验通过 - return { - 'code': 0, - 'msg': '校验通过', - 'data': { - 'nicheng': dashou_profile.nicheng if dashou_profile.nicheng else '', - 'yue': dashou_profile.yue, - 'jifen': dashou_profile.jifen - } - } - - except Exception as e: - # print(f"打手提现校验异常: {e}") - return { - 'code': 99, - 'msg': f'校验过程出错: {str(e)}', - 'data': None - } - - def yanzheng_guanshi_tixian(self, user_main, jine): - """ - 校验管事分红提现条件 - """ - try: - # print(f"开始校验管事提现 - 用户: {user_main.yonghuid}, 金额: {jine}") - - # 查询管事扩展表 - try: - guanshi_profile = UserGuanshi.objects.get(user=user_main) - # print(f"管事信息 - 状态: {guanshi_profile.zhuangtai}, 余额: {guanshi_profile.yue}") - except UserGuanshi.DoesNotExist: - # print("管事扩展表记录不存在") - return { - 'code': 20, - 'msg': '您不是管事身份,无法提现分红', - 'data': None - } - - # 校验1: 管事账号状态 (zhuangtai != 1) - if guanshi_profile.zhuangtai != 1: - return { - 'code': 21, - 'msg': '管事账号已被封禁,无法提现', - 'data': None - } - - # 校验2: 余额是否足够 (yue < jine) - if guanshi_profile.yue < jine: - return { - 'code': 22, - 'msg': f'余额不足,当前余额: {guanshi_profile.yue},提现金额: {jine}', - 'data': None - } - - # print("管事提现校验通过") - - # 所有校验通过 - return { - 'code': 0, - 'msg': '校验通过', - 'data': { - 'yue': guanshi_profile.yue - } - } - - except Exception as e: - # print(f"管事提现校验异常: {e}") - return { - 'code': 99, - 'msg': f'校验过程出错: {str(e)}', - 'data': None - } - - - # 🔥【组长支持】新增组长校验函数 - def yanzheng_zuzhang_tixian(self, user_main, jine): - """ - 校验组长分红提现条件 - """ - try: - # 查询组长扩展表 - try: - zuzhang_profile = UserZuzhang.objects.get(user=user_main) - except UserZuzhang.DoesNotExist: - return { - 'code': 30, - 'msg': '您不是组长身份,无法提现组长分红', - 'data': None - } - - # 校验1: 组长账号状态 (zhuangtai != 1) - if zuzhang_profile.zhuangtai != 1: - return { - 'code': 31, - 'msg': '组长账号已被封禁,无法提现', - 'data': None - } - - # 校验2: 可提现金额是否足够 - if zuzhang_profile.ketixian_jine < jine: - return { - 'code': 32, - 'msg': f'组长可提现金额不足,当前可提: {zuzhang_profile.ketixian_jine},申请金额: {jine}', - 'data': None - } - - # 所有校验通过 - return { - 'code': 0, - 'msg': '校验通过', - 'data': { - 'nicheng': user_main.nicheng if hasattr(user_main, 'nicheng') else '', - 'ketixian_jine': zuzhang_profile.ketixian_jine - } - } - - except Exception as e: - return { - 'code': 99, - 'msg': f'组长提现校验过程出错: {str(e)}', - 'data': None - }''' @@ -3218,113 +2204,7 @@ class TixianShenqingView(APIView): -'''class TixianJiluHuoquViewV2(APIView): - """ - 提现记录获取接口(优化版) - 使用更高效的查询方式,减少数据库压力 - """ - 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) - - # 参数验证 - 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: - return Response({ - 'code': 1, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 限制最大查询数量 - if limit > 100: - limit = 100 - - # 计算偏移量 - offset = (page - 1) * limit - - # 🔴【修改开始】添加必要字段:id, phone, skzhanghao, zhifu - records_queryset = Tixianjilu.objects.filter( - yonghuid=yonghuid - ).order_by('-create_time').values( - 'id', # 🔴【关键】添加ID字段 - 'leixing', - 'jine', - 'zhuangtai', - 'create_time', - 'fangshi', - 'phone', # 🔴【新增】手机号 - 'skzhanghao', # 🔴【新增】收款账号 - 'zhifu' , # 🔴【新增】收款码图片 - 'bhliyou' - )[offset:offset + limit + 1] # 🔴【修复】使用limit + 1 - - # 转换为列表 - records_list = list(records_queryset) - records_count = len(records_list) - - # 判断是否有更多数据 - has_more = records_count > limit - - # 如果查询到的数量大于limit,只返回limit条 - if has_more: - records_to_return = records_list[:limit] - else: - records_to_return = records_list - - # 格式化数据 - formatted_records = [] - for record in records_to_return: - formatted_records.append({ - 'tixian_id': record['id'], # 🔴【关键】使用tixian_id作为字段名 - 'leixing': record['leixing'], - 'jine': float(record['jine']) if record['jine'] else 0.00, - '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 '', - }) - - # 构建响应 - response_data = { - 'list': formatted_records, - 'has_more': has_more, - 'page': page, - 'limit': limit, - 'current_count': len(formatted_records) - } - - 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 TixianJiluHuoquViewV2(APIView): @@ -3542,12 +2422,7 @@ class AdminLoginView(APIView): # 处理IP地址长度问题 # 如果原ip字段长度不够,截取前11个字符(保持兼容性) - '''if len(kehuduan_ip) > 11: - # 如果ip字段长度限制是11,我们截取前11个字符 - # 建议在数据库中修改ip字段为50长度 - cunchu_ip = kehuduan_ip[:11] - else: - cunchu_ip = kehuduan_ip''' + cunchu_ip = kehuduan_ip #print(cunchu_ip) @@ -4822,399 +3697,12 @@ class CfGuanLi(APIView): -'''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=0).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]) - - # 14. 构建返回数据 - chufa_data = [] - for chufa in chufa_list: - 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 - }) - - # 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=0 # 只查询待处理状态的记录 - ).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 != 0: - return Response( - {'code': 400, 'message': '该处罚记录已处理', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 10. 🔴 修复:检查是否有重复的待处理记录,并记录警告 - duplicate_count = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - sqzhuangtai=0 - ).count() - - if duplicate_count > 1: - # 记录警告日志 - logger.warning(f"订单ID {dingdan_id} 存在 {duplicate_count} 条待处理处罚记录,处理最新的一条") - - # 🔴 可选:将其他重复的待处理记录标记为已驳回 - # 防止用户重复提交相同订单的处罚申请 - duplicate_records = Chufajilu.objects.filter( - dingdan_id=dingdan_id, - sqzhuangtai=0 - ).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 扣除打手积分(确保积分不为负数) - jifen_kouchu = chufajilu.jifen - if jifen_kouchu > 0: - # 检查积分是否足够 - if dashou.jifen < jifen_kouchu: - # 如果积分不足,设置为0 - dashou.jifen = 0 - else: - dashou.jifen -= jifen_kouchu - dashou.save() - - # 11.4 更新处罚记录 - chufajilu.sqzhuangtai = 1 # 已处罚 - chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号 - chufajilu.save() - - # 11.5 更新商家订单扩展表 - 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},扣除积分={jifen_kouchu}") - - elif caozuo == 2: - # 驳回处罚 - # 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 - )''' @@ -5973,69 +4461,6 @@ class AdcjxgView(APIView): 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)) - - if huiyuan_id and days > 0: - if days < 1 or 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: - # 续费:延长到期时间 - if existing_record.daoqi_time < timezone.now(): - # 已过期,从当前时间开始算 - new_daoqi_time = timezone.now() + timedelta(days=days) - else: - # 未过期,从原到期时间开始算 - new_daoqi_time = existing_record.daoqi_time + timedelta(days=days) - - 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 - modify_record_data['qitashuoming'] += f' 续费会员 {huiyuan.jieshao} {days}天' - else: - # 新购买 - 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()''' # 🔥🔥🔥 后端修改:支持正负数会员天数 # 修改会员处理逻辑部分 @@ -6477,87 +4902,7 @@ class AdtixianqkView(APIView): '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) - - # 更新提现记录 - withdrawal.zhuangtai = result - withdrawal.shenheid = request.user.yonghuid - - if result == 3: # 拒绝 - withdrawal.bhliyou = reason - - withdrawal.update_time = timezone.now() - withdrawal.save() - - 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 @@ -7271,9 +5616,6 @@ class WechatLoginAndDashouRegisterView(APIView): return self.fanhuiZhihouDengluXinxi(user_main, yaoqingma_wuxiao=True) # 验证管事状态 - '''if guanshi_profile.zhuangtai != 1: - # 管事账号已被禁用 - return self.fanhuiZhihouDengluXinxi(user_main, guanshi_jinyong=True)''' # 4.3 检查用户是否已是打手 dashou_exists = UserDashou.objects.filter(user=user_main).exists() @@ -7295,21 +5637,8 @@ class WechatLoginAndDashouRegisterView(APIView): ) # 创建商家扩展表 - # 商家昵称需要唯一,这里用 用户ID + "的店铺" 来确保唯一性 - '''shangjia_nicheng = f"{user_main.yonghuid}的店铺" - UserShangjia.objects.create( - user=user_main, - nicheng=shangjia_nicheng, - # 其他字段使用默认值 - )''' # 创建管事扩展表 - '''guanshi_yaoqingma = chuangjianYaoqingma(str(user_main.yonghuid)) - new_guanshi_profile = UserGuanshi.objects.create( - user=user_main, - yaoqingma=guanshi_yaoqingma, - # 其他字段使用默认值 - )''' # 更新原管事的邀请数量 locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk) @@ -8954,319 +7283,6 @@ from dingdan.models import Dingdan, DingdanPingtai logger = logging.getLogger(__name__) -'''class KefuGetOrderListView(APIView): - """ - 客服获取平台订单列表接口(发单平台=1) - 请求参数: - phone: 客服账号 - page: 页码 - pageSize: 每页条数 - dingdan_id: 订单ID(精确搜索) - laoban_id: 老板ID(搜索平台扩展表中的老板) - dashou_id: 打手ID(搜索主表中的接单打手) - leixing: 商品类型ID - zhuangtai: 状态(多个用逗号分隔,如 "1,7") - date: 日期(YYYY-MM-DD),筛选订单创建时间 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - start_total = time.time() - - phone = request.data.get('phone', '').strip() - 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() # 日期筛选 - - if not phone: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - 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 '002ab' not in permissions: - return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) - - # ---------- 查询条件 ---------- - base_q = Q(fadan_pingtai=1) # 平台订单 - - 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) # 按日期筛选 - - # ---------- 统计(仅按发单平台) ---------- - stats = Dingdan.objects.filter(fadan_pingtai=1).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).count(), - '1,7': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai__in=[1,7]).count(), - '2': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=2).count(), - '8': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=8).count(), - '3': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=3).count(), - '4': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=4).count(), - '5': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=5).count(), - '6': Dingdan.objects.filter(fadan_pingtai=1, 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}) - - - - - - - -logger = logging.getLogger(__name__) - -class KefuGetShangjiaOrderListView(APIView): - """ - 客服获取商家订单列表接口(发单平台=2) - 请求参数: - phone: 客服账号 - page: 页码 - pageSize: 每页条数 - dingdan_id: 订单ID(精确搜索) - shangjia_id: 商家ID(搜索商家扩展表中的商家ID) - dashou_id: 打手ID(搜索主表中的接单打手) - leixing: 商品类型ID - zhuangtai: 状态(多个用逗号分隔,如 "1,7") - date: 日期(YYYY-MM-DD),筛选订单创建时间 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - start_total = time.time() - - phone = request.data.get('phone', '').strip() - 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() # 格式 YYYY-MM-DD - - if not phone: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - 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. 检查商家订单管理权限(使用 002ac,如果没有可改用 002ab) - if '002ac' not in permissions: - return Response({'code': 403, 'msg': '您无权查看商家订单列表'}) - - # ---------- 查询条件 ---------- - base_q = Q(fadan_pingtai=2) # 商家订单 - - 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) # 按日期筛选 - - # ---------- 统计(仅按发单平台) ---------- - stats = Dingdan.objects.filter(fadan_pingtai=2).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).count(), - '1,7': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai__in=[1,7]).count(), - '2': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=2).count(), - '8': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=8).count(), - '3': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=3).count(), - '4': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=4).count(), - '5': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=5).count(), - '6': Dingdan.objects.filter(fadan_pingtai=2, 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' # 商家ID - ).order_by('-create_time')[(page-1)*page_size : page*page_size] - - # ---------- 获取商家昵称(如果有) ---------- - # 假设商家昵称在 UserShangjia 扩展表中,通过 shangjia_id 关联 UserMain,再关联 UserShangjia - # 此处简化,实际可根据需要查询 - 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 '' - # 游戏昵称从订单主表的 nicheng 取,或从老板昵称取(商家订单可能没有老板昵称,保持简单) - 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})''' - @@ -10336,84 +8352,7 @@ class KefuRejectRefundView(APIView): -'''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: - order = Dingdan.objects.get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) - - # 4. 验证订单状态为退款审核中(4) - if order.zhuangtai != 4: - return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST) - - # 5. 使用事务更新 - with transaction.atomic(): - # 5.1 更新订单状态为退款失败(6) - order.zhuangtai = 6 - order.clkf = current_user.yonghuid - if jujue_liyou: - order.tkly = jujue_liyou # 存储拒绝理由 - order.save() - - # 5.2 更新退款记录表(如果存在) - try: - refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) - refund_record.sqzhuangtai = 2 # 2表示拒绝 - refund_record.chuliid = phone - refund_record.save() - logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}") - except Tuikuanjilu.DoesNotExist: - pass - except Exception as e: - logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True) - - # 5.3 更新客服统计数据 - kefu.jinrichuli += 1 - kefu.jinyuechuli += 1 - kefu.zongchuli += 1 - kefu.save() - - return Response({'code': 0, 'msg': '拒绝退款成功'})''' @@ -11095,6 +9034,7 @@ from houtai.utils import verify_kefu_permission logger = logging.getLogger(__name__) class KefuUpdateDashouView(APIView): + permission_classes = [IsAuthenticated] """ 客服修改打手信息接口 支持操作: @@ -11365,134 +9305,7 @@ class KefuUpdateDashouView(APIView): return Response({'code': 500, 'msg': '服务器内部错误'}) -'''class KefuUpdateDashouView(APIView): - """ - 客服修改打手信息接口 - 请求:POST /yonghu/kefuxgds - 参数:{ - "phone": "客服手机号", - "uid": "打手ID", - // 以下字段可选,只传需要修改的字段 - "yue": 100.00, - "jifen": 200, - "yajin": 50.00, - "zaixianzhuangtai": 0/1, - "zhanghaozhuangtai": 0/1, - "zhuangtai": 1/2 - } - 认证: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: - 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: - return Response({'code': 400, 'msg': '该用户未注册打手身份'}, status=status.HTTP_400_BAD_REQUEST) - - # 4. 收集要修改的字段 - update_fields = {} - editable_fields = [ - 'yue', 'jifen', 'yajin', - 'zaixianzhuangtai', 'zhanghaozhuangtai', 'zhuangtai' - ] - - for field in editable_fields: - if field in request.data: - value = request.data.get(field) - # 类型转换 - if field in ['yue', 'yajin']: - try: - value = Decimal(str(value)) - except: - return Response({'code': 400, 'msg': f'{field} 格式错误'}, status=status.HTTP_400_BAD_REQUEST) - elif field in ['jifen']: - try: - value = int(value) - except: - return Response({'code': 400, 'msg': f'{field} 必须为整数'}, status=status.HTTP_400_BAD_REQUEST) - elif field in ['zaixianzhuangtai', 'zhanghaozhuangtai', 'zhuangtai']: - try: - value = int(value) - if field == 'zhuangtai' and value not in [1, 2]: - return Response({'code': 400, 'msg': f'{field} 值无效'}, status=status.HTTP_400_BAD_REQUEST) - if field != 'zhuangtai' and value not in [0, 1]: - return Response({'code': 400, 'msg': f'{field} 值无效'}, status=status.HTTP_400_BAD_REQUEST) - except: - return Response({'code': 400, 'msg': f'{field} 必须为数字'}, status=status.HTTP_400_BAD_REQUEST) - update_fields[field] = value - - if not update_fields: - return Response({'code': 400, 'msg': '没有要修改的字段'}, status=status.HTTP_400_BAD_REQUEST) - - # 5. 使用事务更新并记录修改日志 - with transaction.atomic(): - # 保存旧值用于记录(可选) - old_values = {field: getattr(dashou, field) for field in update_fields} - - for field, value in update_fields.items(): - setattr(dashou, field, value) - dashou.save() - - # 记录修改日志 - try: - # 构建修改描述 - desc_parts = [] - for field in update_fields: - old = old_values.get(field) - new = update_fields[field] - desc_parts.append(f"{field}: {old} -> {new}") - desc = "; ".join(desc_parts) - - Xiugaijilu.objects.create( - yonghuid=uid, - xiugaiid=phone, # 客服手机号 - leixing=2, # 打手类型 - qitashuoming=desc, - create_time=timezone.now() - ) - except Exception as e: - logger.error(f"记录修改日志失败: {e}") - # 不影响主流程,但记录错误 - - return Response({'code': 0, 'msg': '修改成功'})''' @@ -11770,183 +9583,7 @@ class KefuGetOrderDetailView(APIView): -'''import time -import logging -from django.db import models -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 -from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia, Dashoutupian, Tuikuanjilu - -logger = logging.getLogger(__name__) - -class KefuGetOrderDetailView(APIView): - """ - 客服获取订单详情接口 - 请求:POST /yonghu/kefuhqddxq - 参数:{"phone": "客服手机号", "dingdan_id": "订单ID"} - 认证:JWT - 返回:code=0 + 订单详情数据 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - start_time = time.time() - - # 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: - 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 预加载扩展表,避免后续 N+1 查询 - dingdan_obj = Dingdan.objects.select_related( - 'pingtai_kuozhan', - 'shangjia_kuozhan' - ).only( - 'dingdan_id', 'zhuangtai', 'fadan_pingtai', 'jine', 'dashou_fencheng', - 'jiedan_dashou_id', 'dashou_liuyan', 'zhiding_id', 'shangpin_id', - 'leixing_id', 'tupian', 'jieshao', 'beizhu', 'tkly', 'nicheng', - 'clkf', 'create_time', 'update_time' - ).get(dingdan_id=dingdan_id) - - except Dingdan.DoesNotExist: - logger.warning(f"订单不存在: {dingdan_id}") - return Response( - {'code': 404, 'msg': '订单不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 4. 构建基础响应数据(主表字段) - 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 '', - } - - # 5. 根据发单平台添加扩展表数据 - 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': '', - } - - # 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] - except Exception as e: - logger.warning(f"查询打手图片失败: {str(e)}") - response_data['dashou_images'] = [] - - # 7. 查询退款理由(从退款记录表取最新一条) - try: - tuikuan_record = Tuikuanjilu.objects.filter( - dingdan_id=dingdan_id - ).order_by('-create_time').first() - if tuikuan_record and tuikuan_record.liyou: - response_data['tuikuan_liyou'] = tuikuan_record.liyou - else: - response_data['tuikuan_liyou'] = '' - except Exception as e: - logger.warning(f"查询退款记录失败: {str(e)}") - response_data['tuikuan_liyou'] = '' - - logger.info(f"订单详情接口耗时: {time.time()-start_time:.3f}s") - return Response({'code': 0, 'data': response_data})''' @@ -12376,137 +10013,7 @@ class KefuForceCompleteView(APIView): -'''class KefuWithdrawListView(APIView): - """ - 客服获取提现审核列表接口 - 请求:POST /yonghu/kefu_txsh_list - 参数:{ - "phone": "客服手机号", - "page": 1, - "page_size": 20, - "status": 1(待审核)/2(已处理), - "type": 1(打手)/2(管事) 可选, - "payment": 1(微信)/2(支付宝) 可选, - "search_uid": "用户ID搜索" 可选, - "date": "YYYY-MM-DD" 可选 按创建日期筛选 - } - 认证:JWT - 返回:{ - "code": 0, - "data": { - "list": [提现记录], - "total": 总数, - "stats": { - "total": 总记录, - "awaiting": 待审核数, - "processed": 已处理数 - } - } - } - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - def post(self, request): - phone = request.data.get('phone', '').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) - - status_filter = request.data.get('status') # 1待审核 2已处理 - type_filter = request.data.get('type') - payment_filter = request.data.get('payment') - search_uid = request.data.get('search_uid', '').strip() - date_filter = request.data.get('date', '').strip() - - if not phone: - return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 客服身份验证 - 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 '005bb' not in permissions: - return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) - - # 构建查询条件 - q = Q() - - if status_filter == 1: - q &= Q(zhuangtai=1) - elif status_filter == 2: - q &= Q(zhuangtai__in=[2, 3]) - - if type_filter: - try: - q &= Q(leixing=int(type_filter)) - except ValueError: - pass - - if payment_filter: - try: - q &= Q(fangshi=int(payment_filter)) - except ValueError: - pass - - if search_uid: - q &= Q(yonghuid=search_uid) - - if date_filter: - q &= Q(create_time__date=date_filter) - - # 统计信息(整体,不考虑分页) - stats = { - 'total': Tixianjilu.objects.filter(q).count(), - 'awaiting': Tixianjilu.objects.filter(q & Q(zhuangtai=1)).count(), - 'processed': Tixianjilu.objects.filter(q & Q(zhuangtai__in=[2, 3])).count(), - } - - # 分页查询 - total = Tixianjilu.objects.filter(q).count() - queryset = Tixianjilu.objects.filter(q).order_by('create_time') # 按申请时间升序 - records = queryset[(page-1)*page_size : page*page_size] - - # 构建返回列表 - list_data = [] - for item in records: - list_data.append({ - 'id': item.id, - '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 else '0.00', - 'zhuangtai': item.zhuangtai, - 'fangshi': item.fangshi, - 'shenheid': item.shenheid or '', - 'bhliyou': item.bhliyou or '', - 'create_time': item.create_time, - 'update_time': item.update_time, - }) - - return Response({ - 'code': 0, - 'data': { - 'list': list_data, - 'total': total, - 'stats': stats - } - })''' from django.db.models import Q, Sum