commit 5e90f7c87c79859f6359c1ad078946e3dc0c5517 Author: XingQue Date: Sun Jun 14 02:38:05 2026 +0800 第一次提交:小程序前端最新版本 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..115cc02 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,31 @@ +/* + * Eslint config file + * Documentation: https://eslint.org/docs/user-guide/configuring/ + * Install the Eslint extension before using this feature. + */ +module.exports = { + env: { + es6: true, + browser: true, + node: true, + }, + ecmaFeatures: { + modules: true, + }, + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + }, + globals: { + wx: true, + App: true, + Page: true, + getCurrentPages: true, + getApp: true, + Component: true, + requirePlugin: true, + requireMiniProgram: true, + }, + // extends: 'eslint:recommended', + rules: {}, +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e173d96 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +miniprogram_npm/ +project.private.config.json +.DS_Store +*.log diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/a_long_dianjing.iml b/.idea/a_long_dianjing.iml new file mode 100644 index 0000000..e2358d9 --- /dev/null +++ b/.idea/a_long_dianjing.iml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..863fa72 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..3351dc2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json new file mode 100644 index 0000000..f8b4888 --- /dev/null +++ b/.vs/ProjectSettings.json @@ -0,0 +1,3 @@ +{ + "CurrentProjectSetting": null +} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 0000000..f5d0a94 --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,7 @@ +{ + "ExpandedNodes": [ + "" + ], + "SelectedNode": "\\app.js", + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/a_long_dianjing/FileContentIndex/adbed671-f010-4e25-b61e-f17273811071.vsidx b/.vs/a_long_dianjing/FileContentIndex/adbed671-f010-4e25-b61e-f17273811071.vsidx new file mode 100644 index 0000000..89c2332 Binary files /dev/null and b/.vs/a_long_dianjing/FileContentIndex/adbed671-f010-4e25-b61e-f17273811071.vsidx differ diff --git a/.vs/a_long_dianjing/v17/.wsuo b/.vs/a_long_dianjing/v17/.wsuo new file mode 100644 index 0000000..3263000 Binary files /dev/null and b/.vs/a_long_dianjing/v17/.wsuo differ diff --git a/.vs/a_long_dianjing/v17/DocumentLayout.json b/.vs/a_long_dianjing/v17/DocumentLayout.json new file mode 100644 index 0000000..c80c276 --- /dev/null +++ b/.vs/a_long_dianjing/v17/DocumentLayout.json @@ -0,0 +1,37 @@ +{ + "Version": 1, + "WorkspaceRootPath": "C:\\\u5FAE\u4FE1\u5F00\u53D1\u5DE5\u5177\\\u4EE3\u7801\\a_long_dianjing\\", + "Documents": [ + { + "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\\u5FAE\u4FE1\u5F00\u53D1\u5DE5\u5177\\\u4EE3\u7801\\a_long_dianjing\\app.js||{14D17961-FE51-464D-9111-C4AF11D7D99A}", + "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:app.js||{14D17961-FE51-464D-9111-C4AF11D7D99A}" + } + ], + "DocumentGroupContainers": [ + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "DocumentGroups": [ + { + "DockedWidth": 200, + "SelectedChildIndex": 0, + "Children": [ + { + "$type": "Document", + "DocumentIndex": 0, + "Title": "app.js", + "DocumentMoniker": "C:\\\u5FAE\u4FE1\u5F00\u53D1\u5DE5\u5177\\\u4EE3\u7801\\a_long_dianjing\\app.js", + "RelativeDocumentMoniker": "app.js", + "ToolTip": "C:\\\u5FAE\u4FE1\u5F00\u53D1\u5DE5\u5177\\\u4EE3\u7801\\a_long_dianjing\\app.js", + "RelativeToolTip": "app.js", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001646|", + "WhenOpened": "2026-01-30T05:12:55.655Z", + "EditorCaption": "" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000..1ff7040 Binary files /dev/null and b/.vs/slnx.sqlite differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9eb715c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 GoEasy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/_init_.py b/_init_.py new file mode 100644 index 0000000..a0b11ba --- /dev/null +++ b/_init_.py @@ -0,0 +1,2 @@ +# 3. 在项目的 `__init__.py` 文件(与manage.py同级)中,添加两行代码 +# 文件路径:a_long_dianjing/__init__.py diff --git a/a_long_dianjing/__init__.py b/a_long_dianjing/__init__.py new file mode 100644 index 0000000..4743afc --- /dev/null +++ b/a_long_dianjing/__init__.py @@ -0,0 +1,8 @@ +""" +阿龙电竞 - Django项目初始化 +确保Celery在Django启动时自动加载 +""" + +from .celery import app as celery_app + +__all__ = ('celery_app',) \ No newline at end of file diff --git a/a_long_dianjing/__pycache__/__init__.cpython-312.pyc b/a_long_dianjing/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e4fa3a9 Binary files /dev/null and b/a_long_dianjing/__pycache__/__init__.cpython-312.pyc differ diff --git a/a_long_dianjing/__pycache__/__init__.cpython-313.pyc b/a_long_dianjing/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..da49ef0 Binary files /dev/null and b/a_long_dianjing/__pycache__/__init__.cpython-313.pyc differ diff --git a/a_long_dianjing/__pycache__/celery.cpython-312.pyc b/a_long_dianjing/__pycache__/celery.cpython-312.pyc new file mode 100644 index 0000000..2b4256a Binary files /dev/null and b/a_long_dianjing/__pycache__/celery.cpython-312.pyc differ diff --git a/a_long_dianjing/__pycache__/celery.cpython-313.pyc b/a_long_dianjing/__pycache__/celery.cpython-313.pyc new file mode 100644 index 0000000..3acab3a Binary files /dev/null and b/a_long_dianjing/__pycache__/celery.cpython-313.pyc differ diff --git a/a_long_dianjing/__pycache__/settings.cpython-313.pyc b/a_long_dianjing/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000..66c3de3 Binary files /dev/null and b/a_long_dianjing/__pycache__/settings.cpython-313.pyc differ diff --git a/a_long_dianjing/__pycache__/urls.cpython-313.pyc b/a_long_dianjing/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..952e131 Binary files /dev/null and b/a_long_dianjing/__pycache__/urls.cpython-313.pyc differ diff --git a/a_long_dianjing/__pycache__/wsgi.cpython-313.pyc b/a_long_dianjing/__pycache__/wsgi.cpython-313.pyc new file mode 100644 index 0000000..8f16618 Binary files /dev/null and b/a_long_dianjing/__pycache__/wsgi.cpython-313.pyc differ diff --git a/a_long_dianjing/asgi.py b/a_long_dianjing/asgi.py new file mode 100644 index 0000000..58db31d --- /dev/null +++ b/a_long_dianjing/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for a_long_dianjing project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'a_long_dianjing.settings') + +application = get_asgi_application() diff --git a/a_long_dianjing/celery.py b/a_long_dianjing/celery.py new file mode 100644 index 0000000..2e75a1e --- /dev/null +++ b/a_long_dianjing/celery.py @@ -0,0 +1,115 @@ +""" +阿龙电竞 - Celery定时任务主配置 +生产环境就绪版本,支持精确延时任务和周期性任务 +""" + +import os +from celery import Celery +from celery.schedules import crontab + +# 设置Django默认设置模块 +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'a_long_dianjing.settings') + +# 创建Celery应用实例 +app = Celery('a_long_dianjing') + +# 从Django settings中加载Celery配置(CELERY_前缀) +app.config_from_object('django.conf:settings', namespace='CELERY') + +# 自动发现所有已注册app中的tasks.py文件 +app.autodiscover_tasks() + +# 配置周期性任务(Celery Beat Schedule) +app.conf.beat_schedule = { + # 🔥 核心任务:订单调度器(每2分钟运行) + 'dispatch_pending_orders': { + 'task': 'dingdan.tasks.dispatch_pending_orders', + 'schedule': crontab(minute='*/2'), + 'options': {'queue': 'order_tasks', 'priority': 5}, + }, + + # 🔥 补偿检查任务(每5分钟运行) + 'check_order_expire_task': { + 'task': 'dingdan.tasks.check_order_expire_task', + 'schedule': crontab(minute='*/5'), + 'options': {'queue': 'order_tasks', 'priority': 3}, + }, + # 1. 每日凌晨0点执行 - 清零任务 + 'daily_reset_task': { + 'task': 'yonghu.tasks.daily_reset_task', + 'schedule': crontab(hour=0, minute=0), # 每天0点 + 'options': { + 'queue': 'periodic_tasks', + 'priority': 5 + }, + 'args': (), + 'kwargs': {} + }, + + # 2. 每月1日凌晨0点执行 - 月度清零任务 + 'monthly_reset_task': { + 'task': 'yonghu.tasks.monthly_reset_task', + 'schedule': crontab(hour=0, minute=0, day_of_month=1), # 每月1日0点 + 'options': { + 'queue': 'periodic_tasks', + 'priority': 5 + }, + 'args': (), + 'kwargs': {} + }, + + # 3. 每5分钟检查一次待处理订单 - 作为补偿机制 + 'check_order_expire_task': { + 'task': 'dingdan.tasks.check_order_expire_task', + 'schedule': crontab(minute='*/5'), # 每5分钟 + 'options': { + 'queue': 'order_tasks', + 'priority': 3 + }, + 'args': (), + 'kwargs': {} + }, + + # 4. 每天凌晨0点5分清理收支记录 + 'daily_sz_reset_task': { + 'task': 'peizhi.tasks.daily_sz_reset_task', + 'schedule': crontab(hour=0, minute=5), # 每天0点5分 + 'options': { + 'queue': 'periodic_tasks', + 'priority': 4 + }, + 'args': (), + 'kwargs': {} + } +} + +# 时区设置 +app.conf.timezone = 'Asia/Shanghai' +app.conf.enable_utc = True + +# 队列路由配置 +app.conf.task_routes = { + 'dingdan.tasks.*': {'queue': 'order_tasks'}, + 'yonghu.tasks.*': {'queue': 'periodic_tasks'}, + 'peizhi.tasks.*': {'queue': 'periodic_tasks'}, +} + +# 任务序列化 +app.conf.accept_content = ['json'] +app.conf.task_serializer = 'json' +app.conf.result_serializer = 'json' + +# 任务超时设置 +app.conf.task_time_limit = 300 # 任务最大执行时间300秒 +app.conf.task_soft_time_limit = 240 # 软超时240秒 + +# Worker并发设置 +app.conf.worker_concurrency = 4 +app.conf.worker_prefetch_multiplier = 1 + +# 任务确认设置 +app.conf.task_acks_late = True +app.conf.worker_disable_rate_limits = True + +# 结果过期时间 +app.conf.result_expires = 3600 # 任务结果保留1小时 \ No newline at end of file diff --git a/a_long_dianjing/management/commands/sync_cross_order_status.py b/a_long_dianjing/management/commands/sync_cross_order_status.py new file mode 100644 index 0000000..f41e1b6 --- /dev/null +++ b/a_long_dianjing/management/commands/sync_cross_order_status.py @@ -0,0 +1,114 @@ +import time +import requests +from concurrent.futures import ThreadPoolExecutor, as_completed +from django.core.management.base import BaseCommand +from django.conf import settings +from dingdan.models import Dingdan +from peizhi.models import Club # 你的俱乐部模型,请确认类名 + + +class Command(BaseCommand): + help = '同步我方派单跨平台订单状态(仅进行中订单,失败直接跳过,不重试)' + + def add_arguments(self, parser): + parser.add_argument('--concurrency', type=int, default=3, help='并发数(默认3)') + parser.add_argument('--batch', type=int, default=0, help='处理前N单(0=全部)') + + def handle(self, *args, **options): + concurrency = options['concurrency'] + batch = options['batch'] + + # 查询待同步订单 + orders = Dingdan.objects.filter( + zhuangtai=2, + dispatch_type=1, + is_cross=1, + partner_club_id__isnull=False, + partner_order_id__isnull=False + ).only('dingdan_id', 'partner_order_id', 'partner_club_id', 'zhuangtai') + + total = orders.count() + self.stdout.write(f'待处理订单总数: {total}') + if batch > 0: + orders = orders[:batch] + self.stdout.write(f'本次仅处理前 {batch} 单') + + SYNC_TOKEN = getattr(settings, 'CROSS_PLATFORM_SYNC_TOKEN', 'xK9mP2vL8qW5rT7y') + session = requests.Session() + session.headers.update({'Content-Type': 'application/json'}) + + # 预加载域名映射 + domain_map = {} + for club in Club.objects.all(): + if club.partner_club_id and club.partner_domain: + domain_map[club.partner_club_id] = club.partner_domain + + updated = success = skip = fail = 0 + start = time.time() + + def process_one(order): + # 获取对方域名 + domain = domain_map.get(order.partner_club_id) + if not domain: + try: + club = Club.objects.filter(partner_club_id=order.partner_club_id).first() + if club and club.partner_domain: + domain = club.partner_domain + domain_map[order.partner_club_id] = domain + except Exception: + pass + if not domain: + return ('skip', '无域名') + + url = domain.rstrip('/') + '/houtai/kptxwztjb' + payload = { + 'token': SYNC_TOKEN, + 'partner_order_id': order.partner_order_id, + 'our_order_id': order.dingdan_id, + 'club_id': order.partner_club_id + } + + try: + resp = session.post(url, json=payload, timeout=5) + if resp.status_code == 200: + data = resp.json() + if data.get('code') == 0: + st = data['data']['status'] + if st in (5, 8): + order.zhuangtai = st + order.save(update_fields=['zhuangtai']) + return ('updated', st) + return ('success', st) + else: + return ('skip', data.get('msg', '对方业务错误')) + else: + # 429、404、500 等全部跳过 + return ('skip', f'HTTP {resp.status_code}') + except requests.exceptions.Timeout: + return ('skip', '超时') + except Exception as e: + return ('skip', str(e)) + + # 并发执行 + with ThreadPoolExecutor(max_workers=concurrency) as executor: + future_map = {executor.submit(process_one, o): o for o in orders} + for future in as_completed(future_map): + order = future_map[future] + try: + res, detail = future.result() + if res == 'updated': + updated += 1 + success += 1 + self.stdout.write(f'✅ 更新: {order.dingdan_id} -> {detail}') + elif res == 'success': + success += 1 + else: + skip += 1 + # 跳过的订单静默处理,不打印,以免刷屏 + except Exception as e: + skip += 1 + + elapsed = time.time() - start + self.stdout.write(self.style.SUCCESS( + f'完成: 成功{success}(更新{updated}), 跳过{skip}, 耗时{elapsed:.1f}s' + )) \ No newline at end of file diff --git a/a_long_dianjing/settings.py b/a_long_dianjing/settings.py new file mode 100644 index 0000000..8ecc838 --- /dev/null +++ b/a_long_dianjing/settings.py @@ -0,0 +1,461 @@ +""" +Django settings for a_long_dianjing project. +开发环境配置 - 参照旧项目结构精简 +""" +from pathlib import Path +from datetime import timedelta +import os + +# 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 = '' # 开发环境可暂用,生产必须改 +DEBUG = True # 开发环境为 True +ALLOWED_HOSTS = ['localhost', '127.0.0.1'] # 根据开发前端地址可添加 + +# Application definition +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + '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', # 跨域中间件放最前 + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', # 默认开启,如API无需可注释 + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + # 'guanapi.middleware.OpLogMiddleware', # 【待确认】这是旧项目的自定义中间件,新项目需要吗? +] + +ROOT_URLCONF = 'a_long_dianjing.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'a_long_dianjing.wsgi.application' + +# Database +# 根据你提供的信息配置 (本地开发,端口3307) +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'a_long_dianjing', # 你创建的数据库名 + 'USER': 'root', # 你的本地MySQL用户名 + 'PASSWORD': 'root', # 你的本地MySQL密码 + 'HOST': 'localhost', + 'PORT': '3307', # 注意端口是3307 + 'OPTIONS': { + 'charset': 'utf8mb4', + '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', + }, +] + +# Internationalization +LANGUAGE_CODE = 'zh-Hans' # 使用中文 +TIME_ZONE = 'Asia/Shanghai' +USE_I18N = True +USE_TZ = False # 保持与旧项目一致,不使用UTC时区 + +# 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配置 +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=36500), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=36500), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': False, + 'UPDATE_LAST_LOGIN': False, + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUDIENCE': None, + 'ISSUER': None, + 'JWK_URL': None, + 'LEEWAY': 0, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', + # 修改这里:使用yonghuid而不是id + 'USER_ID_FIELD': 'yonghuid', + 'USER_ID_CLAIM': 'yonghuid', + 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', + 'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser', + 'JTI_CLAIM': 'jti', +} + +# 添加自定义认证后端 +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', +] + +# 必须指定用户模型(即使它不继承AbstractBaseUser) +AUTH_USER_MODEL = 'yonghu.UserMain' + +# DRF配置(也非常简单) +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ), + 'DEFAULT_THROTTLE_CLASSES': [ + 'rest_framework.throttling.AnonRateThrottle', # 匿名用户限制 + 'rest_framework.throttling.UserRateThrottle', # 登录用户限制 + ], + 'DEFAULT_THROTTLE_RATES': { + 'anon': '100/minute', # 匿名用户每分钟100次 + 'user': '1000/minute', # 登录用户每分钟1000次 + }, +} + + + + + + + + + + + + + + + + +# CORS settings +CORS_ALLOWED_ORIGINS = [ + "http://localhost:8080", # 假设前端开发服务器地址 + "http://127.0.0.1:8080", + "http://localhost:5174", +] +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_METHODS = ( + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "OPTIONS", +) +CORS_ALLOW_HEADERS = [ + "accept", + "accept-encoding", + "authorization", + "content-type", + "dnt", + "origin", + "user-agent", + "x-csrftoken", + "x-requested-with", +] + +# 【待确认】自定义用户模型,你需要在 yonghu/models.py 中创建 class Yonghu +#AUTH_USER_MODEL = 'yonghu.Yonghu' + + +# ==================== 跨平台订单互通配置 ==================== +# 接口认证令牌(双方约定,用于简单验证) +CROSS_PLATFORM_TOKEN = 'jdjdjshwjqk' + +# 对方平台接口基础URL(需根据实际部署修改) +PARTNER_BASE_URL = 'https://partner.com/api' + +# 对方平台各接口路径(可根据实际情况调整,但双方需保持一致) +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' # 退款通知 + +# 我方代表对方发单的商家ID(需在数据库中存在,且具有商家扩展表) +AGENT_SHANGJIA_ID = '9106027' + +# 用于验证的会员ID(根据业务约定) +CROSS_PLATFORM_MEMBER_ID = 'vip_2025' + +# 默认游戏类型ID(当对方未传递时使用) +CROSS_PLATFORM_GAME_TYPE_ID = 1 + +# 允许跨平台的商家ID列表(白名单) +CROSS_PLATFORM_WHITELIST = ['5438898'] + + + + + + + + + + + + + + + + +# OSS配置 + + +COS_SECRET_ID ='' +COS_SECRET_KEY ='' +COS_BUCKET = '' +COS_APPID = '' # 你的腾讯云APPID(julebu-1361527063中的数字部分) + + +COS_REGION = 'ap-shanghai' + + +COS_SCHEME = 'https' # 使用HTTPS +COS_DOMAIN = f'https://{COS_BUCKET}.cos.{COS_REGION}.myqcloud.com' +# settings.py - 在COS配置部分添加 +#COS_APPID = '1361527063' # 你的腾讯云APPID(julebu-1361527063中的数字部分) +COS_STS_DURATION = 7200 # STS临时凭证有效期(秒) + +# ==================== 业务相关配置 (值先空着,结构可保留) ==================== + + + +# 微信虚拟支付配置 +VP_OFFER_ID = '你的offer_id' # 虚拟支付应用ID +VP_APPKEY = '你的现网AppKey' # 正式环境密钥 +VP_APPKEY_SANDBOX = '你的沙箱AppKey' # 沙箱环境密钥 +VP_ENV = 1 # 1=沙箱(测试), 0=正式(上线改这里) + + +# 微信小程序配置 (开发环境可先留空或使用测试号) + +WEIXIN_APPID = '' # 你的小程序APPID +WEIXIN_SECRET = '' + + + + +WEIXIN_MCHID = '' # 商户号,支付功能需要 + + +WEIXIN_SHANGHUMIYAO = ''# 商户密钥,支付功能需要 +# 🆕【新增】服务号配置 +WEIXIN_OFFICIAL_APPID = '' # 服务号APPID +WEIXIN_OFFICIAL_SECRET = '填写你的服务号APPSECRET' # 服务号SECRET +WEIXIN_OFFICIAL_TOKEN = '填写你的Token' # 服务号Token +WEIXIN_OFFICIAL_ENCODING_AES_KEY = '填写你的EncodingAESKey' # 可选 +WEIXIN_TEMPLATE_ID = '' # 模板ID + + +# 罚款缴纳微信支付配置 +FAKUAN_PAY_DESCRIPTION = '罚款缴纳' # 支付描述 +FAKUAN_PAY_NOTIFY_URL = 'https://www.abas.asia/hqhd/yonghu/fk_huitiao' # 微信支付回调地址(根据实际域名调整) + +# 🆕【新增】模板消息配置 +WEIXIN_TEMPLATE_MAX_PER_MINUTE = 950 # 每分钟最大发送量(留50缓冲) +WEIXIN_TEMPLATE_BATCH_SIZE = 100 # 每批发送数量 + +WEIXIN_NOTIFY_URL = 'http:api' # 支付回调地址,如:http://你的域名/api/pay/callback/ +# settings.py 或相应配置文件中 +WEIXIN_PAY_DESCRIPTION = "网络服务" # 支付描述 +# 押金充值相关配置 +YAJIN_PAY_DESCRIPTION = "打手押金充值" # 押金支付描述 +YAJIN_PAY_NOTIFY_URL = "http://你的域名/api/shangpin/yajinhuitiao/" # 押金支付回调地址 + +# 积分充值相关配置 +JIFEN_PAY_DESCRIPTION = "打手积分补充" # 积分支付描述 +JIFEN_PAY_NOTIFY_URL = "http://你的域名/api/shangpin/jifenhuitiao/" # 积分支付回调地址 + +# 会员充值相关配置 +HUIYUAN_PAY_DESCRIPTION = "打手会员充值" # 会员支付描述 +HUIYUAN_PAY_NOTIFY_URL = "http://你的域名/api/shangpin/huiyuanhuitiao/" # 会员支付回调地址 + + +# 考核支付配置 +KAOHE_PAY_DESCRIPTION = '考核金牌-缴纳考核费' +KAOHE_PAY_NOTIFY_URL = 'https://您的域名/hqhd/yonghu/kh_huitiao' # 替换为实际回调地址 +KAOHE_PAY_DEFAULT_FEE = 5.00 # 未配置费用时的默认金额 + + +# settings.py +SHANGJIA_CZ_PREFIX = "CzSJ" # 商家充值订单前缀 +SHANGJIA_CZ_PAY_DESCRIPTION = "阿龙电竞-商家余额充值" +SHANGJIA_CZ_PAY_NOTIFY_URL = "https://yourdomain.com/shangpin/sjhuitiao" +SHANGJIA_CZ_MIN_AMOUNT = 1 +SHANGJIA_CZ_MAX_AMOUNT = 10000 + +# 订单ID前缀配置 +ORDER_PREFIX = { + 'yajin': 'CzYJ', # 押金订单前缀 + 'jifen': 'CzJF', # 积分订单前缀 + 'huiyuan': 'CzHY', # 会员订单前缀 +} + + + + + +# 腾讯云即时通讯 (IM) - 开发环境可暂不配置 +TENCENT_IM_CONFIG = { + 'SDK_APP_ID': 0, # 整数 + 'SECRET_KEY': '', +} + +# 阿里云短信 - 开发环境可暂不配置 +ALIYUN_SMS_CONFIG = { + 'ACCESS_KEY_ID': '', + 'ACCESS_KEY_SECRET': '', + 'SIGN_NAME': '', + 'TEMPLATE_CODE': '', + 'REGION_ID': 'cn-hangzhou', +} + +# HashID (用于加密ID,旧项目有,新项目需要吗?) +# HASHID_FIELD_SALT = 'a_long_dianjing_2025_salt' # 【待确认】请设置你的盐值 + + + # settings.py 尾部添加 +# settings.py +# settings.py +# settings.py 中添加配置 +GOEASY_APPKEY = '' # 你的AppKey +GOEASY_SECRET = '' # 从GoEasy控制台获取 +GOEASY_REST_URL = 'https://rest-hz.goeasy.io/v2/im/message' # 正式环境 + + + + + + +# settings.py +# H5静态页面域名配置 +H5_DOMAIN = 'https://h5.yourdomain.com' # 生产环境 +# H5_DOMAIN = 'https://test-h5.yourdomain.com' # 测试环境 + + + + +# 特殊管事ID列表(yonghuid),这些管事的首次分红金额从 UserGuanshi.fenhong_yici 字段获取 +SPECIAL_GUANSHI_IDS = [ + '1000001', + '1000002', + # ... 按需添加 +] + +# ========== 管理员批量链接白名单 ========== +ADMIN_BATCH_LINK_ALLOWED_PHONES = [ + '13800138000', + '13912345678', +] + +BATCH_LINK_DEFAULT_COUNT = 100 +BATCH_LINK_MAX_COUNT = 200 + + + + + +# settings.py + +# settings.py +WECHAT_PAY_V3_CONFIG = { + 'MCHID': '', # 商户号 + 'APPID': '', # 小程序APPID + 'API_V3_KEY': '', # APIv3密钥(32字节) + 'PRIVATE_KEY_PATH': '/opt/1panel/apps/openresty/openresty/www/sites/119.28.221.247.1199/huizhifuzhengshu/apiclient_key.pem', # 商户私钥文件路径 + 'CERT_SERIAL_NO': '', # 商户证书序列号 + 'PLATFORM_CERT_DIR': '/opt/1panel/apps/openresty/openresty/www/sites/119.28.221.247.1199/huizhifuzhengshu/pub_key.pem', # 存放微信平台证书公钥的目录(文件名:序列号.pem) + 'NOTIFY_URL': 'https://ios808.top/hqhd/yonghu/tixiancallback', # 回调地址 + 'TRANSFER_SCENE_ID': '', # 替换为你的实际场景ID +} + + + + +CROSS_PLATFORM_SYNC_TOKEN = 'xK9mP2vL8qW5rT7y' + +# 提现收款链路日志(输出到 stderr,gunicorn/1panel 可抓取) +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'tixian_shenhe_console': { + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + }, + 'formatters': { + 'verbose': { + 'format': '[{asctime}] {levelname} {name} {message}', + 'style': '{', + }, + }, + 'loggers': { + 'yonghu.tixian_shenhe': { + 'handlers': ['tixian_shenhe_console'], + 'level': 'INFO', + 'propagate': False, + }, + }, +} \ No newline at end of file diff --git a/a_long_dianjing/urls.py b/a_long_dianjing/urls.py new file mode 100644 index 0000000..3b65b21 --- /dev/null +++ b/a_long_dianjing/urls.py @@ -0,0 +1,60 @@ +""" +URL configuration for a_long_dianjing project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/6.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +'''from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +]''' + +# a_long_dianjing/urls.py +""" +项目根路由配置 - 按照生产环境格式,连接所有APP路由 +注意:本项目使用MinIO模拟OSS,文件上传不经过Django MEDIA_URL,所以不需要static(settings.MEDIA_URL)配置 +""" + +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + # Django管理后台 + path('admin/', admin.site.urls), + + # 用户相关路由 + path('yonghu/', include('yonghu.urls')), + + # 订单相关路由 + path('dingdan/', include('dingdan.urls')), + + # 商品/陪玩服务相关路由 + path('shangpin/', include('shangpin.urls')), + + + # 配置相关路由(系统配置) + path('peizhi/', include('peizhi.urls')), + + path('houtai/', include('houtai.urls')), + path('shangdian/', include('shangdian.urls')), + path('dengji/', include('dengji.urls')), + +] + +# 重要说明: +# 1. 本项目使用MinIO(模拟OSS)作为对象存储,文件上传通过MinIO的S3接口直接处理 +# 2. 文件存储不经过Django的MEDIA_URL,所以不需要配置 static(settings.MEDIA_URL, ...) +# 3. 未来迁移到腾讯云COS/阿里云OSS时,也通过django-storages直接处理,不经过Django静态文件服务 +# 4. 所有文件访问将通过MinIO/COS的CDN域名直接访问,不需要Django路由转发 diff --git a/a_long_dianjing/wsgi.py b/a_long_dianjing/wsgi.py new file mode 100644 index 0000000..8d00672 --- /dev/null +++ b/a_long_dianjing/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for a_long_dianjing project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'a_long_dianjing.settings') + +application = get_wsgi_application() diff --git a/app.js b/app.js new file mode 100644 index 0000000..cba1fee --- /dev/null +++ b/app.js @@ -0,0 +1,325 @@ +// app.js - 优化生产版本(修复重连机制和用户ID问题) +import GoEasy from './static/lib/goeasy-2.13.24.esm.min'; +const ChatCore = require('./utils/chat-core'); +import { check } from './utils/phone-auth'; + + +// ===== 角色 → 默认首页(与 custom-tab-bar 完全一致) ===== +const roleDefaultPage = { + normal: '/pages/index/index', + dashou: '/pages/dashouduan/dashouduan', + shangjia: '/pages/shangjiaduan/shangjiaduan', // 商家首页也是商城 + guanshi: '/pages/guanshiduan/guanshiduan', + zuzhang: '/pages/zuzhangduan/zuzhangduan', + kaoheguan: '/pages/kaoheguan/kaoheguan' +}; + +App({ + globalData: { + + hasShownPopupOnColdStart: false, + + apiBaseUrl: 'https://6ccy.top/hqhd', + ossImageUrl: '', + morentouxiang: '', + dashouguize: '', + + shangpinliebiao: [], + shangpinzhuanqu: [], + shangpinleixing: [], + shangpinlunbo: [], + shangpingonggao: '', + lunbozhanwei: '/images/lunbozhanwei.jpg', + + dingdanTiaoshu: { + daifuwu: 0, + fuwuzhong: 0, + yiwancheng: 0, + yituikuan: 0 + }, + + dashouqun: '', + dashouqunid: '', + guanshiqun: '', + guanshiqunid: '', + + appId:'wx2517043ca3f39677', + + shangjiastatus: 0, + dashoustatus: 0, + guanshistatus: 0, + dashouNicheng: '', + zhanghaoStatus: null, + dashouzhuangtai: null, + yongjin: null, + zonge: null, + yajin: null, + chenghao: '', + jinfen: null, + clumber: [{ + huiyuanid: '', + huiyuanming: '', + daoqi: '' + }], + chengjiaoliang: null, + zaixianZhuangtai: null, + + guanshi: { + nicheng: '', + uid: '', + touxiang: '', + gszhstatus: '', + yaoqingzongshu: 0, + fenyongzonge: '0.00', + fenyongtixian: '0.00' + }, + + shangjia: { + sjzhzhuangtai: '', + sjyue: '', + fadanzong: null, + tuikuanzong: null, + riliushui: '', + yueliushui: '' + }, + + liaotian_liebiao: [], + xshenfen: 1, + + goEasyConfig: null, + kefuConfig: { + link: '', + enterpriseId: '' + }, + + cosConfig: { + bucket: '', + region: 'ap-shanghai', + uploadPathPrefix: 'order/' + }, + + goEasyConnection: { + status: 'disconnected', + userId: '', + identityType: '', + lastConnectTime: 0, + autoReconnect: true, + reconnectAttempts: 0, + maxReconnectAttempts: 5, + heartbeatInterval: null, + cacheKeys: { + savedConnection: 'savedGoEasyConnection', + userId: 'goEasyUserId', + identityType: 'currentGoEasyIdentity', + connectTime: 'goEasyConnectTime' + }, + config: { + heartbeatInterval: 20000, + reconnectDelay: 3000, + offlineTimeout: 30000, + cacheValidityHours: 12 + } + }, + + messageManager: { + unreadTotal: 0, + unreadMap: {}, + latestMessages: [], + notificationQueue: [], + notificationVisible: false, + lastNotificationTime: 0, + notificationCooldown: 3000, + tabBarIndex: 2, + showTabBarBadge: true, + customTabBar: true, + tabBarBadgeText: 0, + soundEnabled: true, + vibrationEnabled: true, + doNotDisturb: false, + doNotDisturbStart: '22:00', + doNotDisturbEnd: '08:00', + notificationStyle: { + position: 'top', + duration: 3000, + backgroundColor: '#00f7ff', + textColor: '#ffffff', + borderRadius: '16rpx', + boxShadow: '0 10rpx 30rpx rgba(0, 247, 255, 0.3)' + }, + currentNotification: null, + cacheKeys: { + messageSettings: 'messageSettings', + unreadTotal: 'messageUnreadTotal', + latestMessages: 'latestMessages', + notificationMuted: 'notificationMuted' + } + }, + + pageState: { + currentPage: '', + isInChatPage: false, + currentChatId: '', + lastPageUpdate: 0 + }, + + eventListeners: {}, + debugMode: false, + currentUser: null, + currentRole: 'normal' // 当前角色,默认老板 + }, + + // ========== 仅修改 onLaunch,其余未动 ========== + async onLaunch() { + // ---------- ① 立即隐藏“返回首页”按钮 ---------- + wx.hideHomeButton(); + wx.onAppRoute(() => { + wx.hideHomeButton(); + }); + // 🆕 手机号认证检查 + + + // ---------- ② 恢复角色并跳转(不依赖网络) ---------- + const savedRole = wx.getStorageSync('currentRole') || 'normal'; + this.globalData.currentRole = savedRole; + + const targetPage = roleDefaultPage[savedRole] || '/pages/index/index'; + if (savedRole !== 'normal' || targetPage !== '/pages/index/index') { + setTimeout(() => { + wx.reLaunch({ url: targetPage }); + }, 0); + } + + // ---------- ③ 后台静默获取配置,成功后再初始化 GoEasy/IM ---------- + this.fetchDynamicConfig() + .then(config => { + this.applyDynamicConfig(config); + this.initGoEasyWithConfig(); + ChatCore.initGlobalMessageSystem(this); + this.initCurrentUser(); + + const saved = this.getSavedConnection(); + console.log('【app.js启动】缓存身份:', saved ? saved.identityType : '无', + 'userId:', saved ? saved.userId : '无'); + setTimeout(() => { + this.ensureConnection(); + }, 300); + }) + .catch(error => { + console.error('获取动态配置失败:', error); + + // 兜底初始化,防止后续调用报错 + this.initGoEasyWithConfig(); + ChatCore.initGlobalMessageSystem(this); + this.initCurrentUser(); + }); + + + // ---------- ④ 所有初始化完成后,最后进行手机号认证检查 ---------- + try { + const needAuth = await check(); // 你已经顶部 import 了 + if (needAuth) { + wx.reLaunch({ url: '/pages/phone-auth/phone-auth' }); + return; + } + } catch (e) {} + + }, + + + + + // 以下三个方法完全保持原样 + fetchDynamicConfig() { + return new Promise((resolve, reject) => { + wx.request({ + url: this.globalData.apiBaseUrl + '/peizhi/qdpzhq', + method: 'POST', + header: { 'content-type': 'application/json' }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 0) { + resolve(res.data.data); + } else { + reject(new Error(res.data?.msg || '接口返回错误')); + } + }, + fail: reject + }); + }); + }, + + applyDynamicConfig(config) { + if (config.cos) { + this.globalData.ossImageUrl = config.cos.ossImageUrl || ''; + this.globalData.cosConfig = { + bucket: config.cos.bucket || '', + region: config.cos.region || 'ap-shanghai', + uploadPathPrefix: config.cos.uploadPathPrefix || 'order/' + }; + } + if (config.goEasy) { + this.globalData.goEasyConfig = { + host: config.goEasy.host || 'hangzhou.goeasy.io', + appkey: config.goEasy.appkey || '' + }; + } + if (config.otherConfig) { + this.globalData.morentouxiang = config.otherConfig.morentouxiang || ''; + this.globalData.dashouguize = config.otherConfig.dashouguize || ''; + } + if (config.kefu) { + this.globalData.kefuConfig = { + link: config.kefu.link || '', + enterpriseId: config.kefu.enterpriseId || '' + }; + } + }, + + initGoEasyWithConfig() { + const cfg = this.globalData.goEasyConfig; + if (!cfg || !cfg.appkey) { + console.error('GoEasy appkey 未配置,无法初始化'); + return; + } + wx.goEasy = GoEasy.getInstance({ + host: cfg.host || 'hangzhou.goeasy.io', + appkey: cfg.appkey, + modules: ['im', 'pubsub'] + }); + wx.GoEasy = GoEasy; + }, + + initCurrentUser() { + const uid = wx.getStorageSync('uid'); + if (uid) { + this.globalData.currentUser = { + id: uid, + name: '用户' + uid.substring(0, 6), + avatar: this.globalData.ossImageUrl + this.globalData.morentouxiang + }; + } + }, + + emitEvent(eventName, data) { + if (this.globalData.eventListeners[eventName]) { + this.globalData.eventListeners[eventName].forEach(callback => { + try { callback(data); } catch (error) { console.error(error); } + }); + } + }, + + on(eventName, callback) { + if (!this.globalData.eventListeners[eventName]) { + this.globalData.eventListeners[eventName] = []; + } + this.globalData.eventListeners[eventName].push(callback); + }, + + off(eventName, callback) { + if (this.globalData.eventListeners[eventName]) { + const index = this.globalData.eventListeners[eventName].indexOf(callback); + if (index > -1) { + this.globalData.eventListeners[eventName].splice(index, 1); + } + } + } +}); \ No newline at end of file diff --git a/app.json b/app.json new file mode 100644 index 0000000..927550b --- /dev/null +++ b/app.json @@ -0,0 +1,107 @@ +{ + "pages": [ + "pages/index/index", + "pages/fenlei/fenlei", + "pages/xuanren/xuanren", + "pages/xiaoxi/xiaoxi", + "pages/wode/wode", + "pages/shangpinxiangqing/shangpinxiangqing", + "pages/tijiao/tijiao", + "pages/xiugai/xiugai", + "pages/dashouduan/dashouduan", + "pages/shangjiaduan/shangjiaduan", + "pages/guanshiduan/guanshiduan", + "pages/dingdan/dingdan", + "pages/dingdanxiangqing/dingdanxiangqing", + "pages/dashouguize/dashouguize", + "pages/dashouxiugai/dashouxiugai", + "pages/dashoupaihang/dashoupaihang", + "pages/dashouchongzhi/dashouchongzhi", + "pages/dashouxiaoxi/dashouxiaoxi", + "pages/dashoudingdan/dashoudingdan", + "pages/jiedan/jiedan", + "pages/tixian/tixian", + "pages/yaoqingdashou/yaoqingdashou", + "pages/wodedashou/wodedashou", + "pages/czjilu/czjilu", + "pages/guanshipaihang/guanshipaihang", + "pages/sjpaidan/sjpaidan", + "pages/sjdingdan/sjdingdan", + "pages/sjxiaoxi/sjxiaoxi", + "pages/sjpaihang/sjpaihang", + "pages/sjchongzhi/sjchongzhi", + "pages/sjddxq/sjddxq", + "pages/dsddxq/dsddxq", + "pages/liaotian/liaotian", + "pages/jisufd/jisufd", + "pages/jiedanchi/jiedanchi", + "pages/haibao/haibao", + "pages/jinpaids/jinpaids", + "pages/zuzhangduan/zuzhangduan", + "pages/guanzhual/guanzhual", + "pages/zzfhjilu/zzfhjilu", + "pages/yqguanshi/yqguanshi", + "components/global-notification/global-notification", + "components/popup-notice/popup-notice", + "pages/tixian/components/mode1/mode1", + "pages/tixian/components/mode2/mode2", + + "pages/jiedanchi2/jiedanchi2", + "pages/qunliaotian/qunliaotian", + "pages/kefuliaotian/kefuliaotian", + "pages/renzheng/renzheng", + + "components/order-card/order-card", + "components/order-sender/order-sender", + "pages/cfss/cfss/cfss", + "pages/cfss/components/fakuan-list/fakuan-list", + "pages/cfss/components/jifen-list/jifen-list", + "pages/cfss/components/fakuan-pay/fakuan-pay", + "pages/phone-auth/phone-auth", + "components/chenghao-tag/chenghao-tag", + "pages/kaoheguan/kaoheguan", + "pages/kaohe_dafen/kaohe_dafen", + "pages/kaohe_jilu/kaohe_jilu", + "pages/kaohe_zhongxin/kaohe_zhongxin", + "pages/kaohe_jinpai/kaohe_jinpai" + + ], + + "usingComponents": { + "popup-notice": "/components/popup-notice/popup-notice", + "custom-tab-bar": "/custom-tab-bar/index" + }, + + "window": { + "navigationBarTextStyle": "black", + "navigationStyle": "default", + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#fff", + "navigationBarTitleText": "二H电竞" + }, + "tabBar": { + "custom": true, + "list": [ + + + { + "pagePath": "pages/jiedanchi/jiedanchi", + "text": "接单池", + "iconPath": "/images/jiedanchi.png", + "selectedIconPath": "/images/jiedanchi.png" + }, + { + "pagePath": "pages/jiedanchi2/jiedanchi2", + "text": "接单池", + "iconPath": "/images/jiedanchi.png", + "selectedIconPath": "/images/jiedanchi.png" + } + + ] + }, + + + "style": "v2", + "sitemapLocation": "sitemap.json", + "lazyCodeLoading": "requiredComponents" +} \ No newline at end of file diff --git a/app.wxss b/app.wxss new file mode 100644 index 0000000..06c6fc9 --- /dev/null +++ b/app.wxss @@ -0,0 +1,10 @@ +/**app.wxss**/ +.container { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 200rpx 0; + box-sizing: border-box; +} diff --git a/components/chenghao-tag/chenghao-tag.js b/components/chenghao-tag/chenghao-tag.js new file mode 100644 index 0000000..ae79133 --- /dev/null +++ b/components/chenghao-tag/chenghao-tag.js @@ -0,0 +1,73 @@ +// components/chenghao-tag/chenghao-tag.js +const app = getApp(); + +Component({ + properties: { + mingcheng: { type: String, value: '' }, + texiaoJson: { type: Object, value: {} } + }, + data: { + // 默认值:宽152,高52,六边形,无背景图,无动画,白色字 + width: 152, + height: 52, + shapeClass: 'liubianxing', // 保证至少不是矩形 + animationClass: '', + bgStyle: '', + textColor: '#FFFFFF', + textSize: 22, + imageUrl: '' + }, + lifetimes: { + attached() { + const cfg = this.properties.texiaoJson || {}; + + // 1. 尺寸(稍大一些,一行能放3~4个) + const width = cfg.width || 152; + const height = cfg.height || 52; + + // 2. 背景处理:优先背景图,否则用渐变/纯色 + let bgStyle = ''; + let imageUrl = ''; + if (cfg.image_url) { + const ossImageUrl = app.globalData.ossImageUrl || ''; + imageUrl = cfg.image_url.startsWith('http') + ? cfg.image_url + : ossImageUrl + cfg.image_url; + } else if (cfg.bg_gradient) { + bgStyle = `background: ${cfg.bg_gradient};`; + } else if (cfg.bg_color) { + bgStyle = `background: ${cfg.bg_color};`; + } else { + // 默认金橙渐变 + bgStyle = `background: linear-gradient(135deg, #FFD700, #FF8C00);`; + } + + // 3. 形状(后端传入 shape 字段,默认 liubianxing) + const shapeClass = cfg.shape || 'liubianxing'; + + // 4. 动画 + const animationClass = cfg.animation || ''; + + // 5. 文字 + const textColor = cfg.text_color || '#FFFFFF'; + const textSize = cfg.text_size || 22; + + this.setData({ + width, height, + bgStyle, imageUrl, + shapeClass, animationClass, + textColor, textSize + }); + } + }, + methods: { + // 背景图加载失败时回退为纯色背景 + onImageError() { + this.setData({ imageUrl: '' }); + // 如果 bgStyle 为空,给个默认背景 + if (!this.data.bgStyle) { + this.setData({ bgStyle: 'background: linear-gradient(135deg, #FFD700, #FF8C00);' }); + } + } + } +}); \ No newline at end of file diff --git a/components/chenghao-tag/chenghao-tag.json b/components/chenghao-tag/chenghao-tag.json new file mode 100644 index 0000000..62389a9 --- /dev/null +++ b/components/chenghao-tag/chenghao-tag.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} + } \ No newline at end of file diff --git a/components/chenghao-tag/chenghao-tag.wxml b/components/chenghao-tag/chenghao-tag.wxml new file mode 100644 index 0000000..e9c44f3 --- /dev/null +++ b/components/chenghao-tag/chenghao-tag.wxml @@ -0,0 +1,20 @@ + + + + + + + {{mingcheng}} + \ No newline at end of file diff --git a/components/chenghao-tag/chenghao-tag.wxss b/components/chenghao-tag/chenghao-tag.wxss new file mode 100644 index 0000000..d142188 --- /dev/null +++ b/components/chenghao-tag/chenghao-tag.wxss @@ -0,0 +1,90 @@ +/* components/chenghao-tag/chenghao-tag.wxss */ + +/* ========== 根容器 ========== */ +.tag-root { + display: inline-flex; + align-items: center; + justify-content: center; + position: relative; + border-radius: 4rpx; + overflow: hidden; + flex-shrink: 0; + margin: 4rpx; + box-sizing: border-box; + } + + /* ========== 背景图 ========== */ + .tag-bg-image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; + border-radius: inherit; + } + + /* ========== 文字层 ========== */ + .tag-text { + position: relative; + z-index: 2; + font-weight: bold; + white-space: nowrap; + text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.5); + padding: 0 10rpx; + } + + /* ========== 多边形形状 ========== */ + .tag-root.liubianxing { + clip-path: polygon(12% 0%, 88% 0%, 100% 50%, 88% 100%, 12% 100%, 0% 50%); + } + .tag-root.lingxing { + clip-path: polygon(50% 0%, 85% 12%, 100% 50%, 85% 88%, 50% 100%, 15% 88%, 0% 50%, 15% 12%); + } + .tag-root.wujiaoxing { + clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%); + } + .tag-root.dunpai { + clip-path: polygon(18% 0%, 82% 0%, 100% 18%, 95% 50%, 100% 82%, 82% 100%, 18% 100%, 0% 82%, 5% 50%, 0% 18%); + } + .tag-root.jiantou { + clip-path: polygon(0% 0%, 85% 0%, 100% 50%, 85% 100%, 0% 100%, 12% 50%); + } + + /* ========== 动画 ========== */ + /* 流光扫描 */ + .tag-root.shine::after { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 60%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.5), transparent); + transform: skewX(-20deg); + animation: shine-anim 2s infinite; + z-index: 1; + pointer-events: none; + } + @keyframes shine-anim { + 0% { left: -100%; } + 100% { left: 200%; } + } + + /* 呼吸光晕 */ + .tag-root.pulse { + animation: pulse-anim 1.5s ease-in-out infinite; + } + @keyframes pulse-anim { + 0%, 100% { filter: brightness(1); transform: scale(1); } + 50% { filter: brightness(1.2); transform: scale(1.04); } + } + + /* 外发光闪烁 */ + .tag-root.glow { + animation: glow-anim 2s ease-in-out infinite; + } + @keyframes glow-anim { + 0%, 100% { box-shadow: 0 0 8rpx rgba(255, 215, 0, 0.4); } + 50% { box-shadow: 0 0 20rpx rgba(255, 215, 0, 0.8), 0 0 40rpx rgba(255, 215, 0, 0.4); } + } \ No newline at end of file diff --git a/components/global-notification/global-notification.js b/components/global-notification/global-notification.js new file mode 100644 index 0000000..bd88ab6 --- /dev/null +++ b/components/global-notification/global-notification.js @@ -0,0 +1,366 @@ +Component({ + properties: { + position: { + type: String, + value: 'top' + }, + duration: { + type: Number, + value: 3000 + }, + showProgress: { + type: Boolean, + value: true + }, + showMuteButton: { + type: Boolean, + value: true + }, + showTime: { + type: Boolean, + value: true + }, + swipeToClose: { + type: Boolean, + value: true + }, + swipeThreshold: { + type: Number, + value: 50 + } + }, + + data: { + show: false, + title: '', + message: '', + avatar: '', + notificationData: null, + progress: 100, + progressInterval: null, + positionClass: 'top', + + // 滑动相关 + touchStartY: 0, + touchStartX: 0, + currentY: 0, + currentX: 0, + isSwiping: false, + swipingClass: '', + swipeStyle: '', + swipeDistance: 0, + + // 静音状态 + isMuted: false, + + // 自动隐藏计时器 + autoHideTimeout: null + }, + + lifetimes: { + attached() { + this.loadMuteSetting(); + this.setData({ + positionClass: this.properties.position + }); + this.setupDirectEventListeners(); + }, + + detached() { + this.clearTimers(); + } + }, + + methods: { + setupDirectEventListeners() { + const app = getApp(); + if (app) { + const self = this; + const globalNotification = { + show: (data) => { + if (self && self.showNotification) { + self.showNotification(data); + } + }, + hide: () => { + if (self && self.hideNotification) { + self.hideNotification(); + } + }, + isMuted: () => { + return self.data.isMuted; + } + }; + app.globalData.globalNotification = globalNotification; + } + }, + + loadMuteSetting() { + try { + const isMuted = wx.getStorageSync('notificationMuted') || false; + this.setData({ isMuted }); + } catch (error) { + console.warn('加载静音设置失败:', error); + } + }, + + saveMuteSetting(isMuted) { + try { + wx.setStorageSync('notificationMuted', isMuted); + } catch (error) { + console.warn('保存静音设置失败:', error); + } + }, + + showNotification(data) { + if (!data) return; + if (this.data.isMuted) return; + + this.clearTimers(); + + const app = getApp(); + let avatar = data.avatar; + if (!avatar && app && app.globalData) { + avatar = app.globalData.ossImageUrl + app.globalData.morentouxiang; + } + + this.setData({ + show: true, + title: data.senderName || '新消息', + message: data.content || '[消息内容]', + avatar: avatar, + notificationData: data, + progress: 100, + swipingClass: '', + swipeStyle: '' + }); + + if (this.properties.showProgress) { + this.startProgressBar(this.properties.duration); + } + + this.data.autoHideTimeout = setTimeout(() => { + this.hideNotification(); + }, this.properties.duration); + }, + + hideNotification() { + this.clearTimers(); + this.setData({ + show: false, + progress: 100, + swipingClass: '', + swipeStyle: '' + }); + this.triggerEvent('hide'); + }, + + startProgressBar(duration) { + const interval = 100; + const steps = duration / interval; + const stepValue = 100 / steps; + + let currentProgress = 100; + this.data.progressInterval = setInterval(() => { + currentProgress -= stepValue; + if (currentProgress <= 0) { + currentProgress = 0; + this.setData({ progress: currentProgress }); + clearInterval(this.data.progressInterval); + } else { + this.setData({ progress: currentProgress }); + } + }, interval); + }, + + clearTimers() { + if (this.data.autoHideTimeout) { + clearTimeout(this.data.autoHideTimeout); + this.data.autoHideTimeout = null; + } + if (this.data.progressInterval) { + clearInterval(this.data.progressInterval); + this.data.progressInterval = null; + } + }, + + onTouchStart(e) { + if (!this.properties.swipeToClose) return; + const touch = e.touches[0]; + this.setData({ + touchStartY: touch.clientY, + touchStartX: touch.clientX, + currentY: touch.clientY, + currentX: touch.clientX, + isSwiping: false + }); + this.clearTimers(); + }, + + onTouchMove(e) { + if (!this.properties.swipeToClose) return; + const touch = e.touches[0]; + const deltaY = touch.clientY - this.data.touchStartY; + const deltaX = touch.clientX - this.data.touchStartX; + const absDeltaY = Math.abs(deltaY); + const absDeltaX = Math.abs(deltaX); + + if (absDeltaY > 10 && absDeltaY > absDeltaX) { + this.setData({ + isSwiping: true, + currentY: touch.clientY, + swipingClass: deltaY < 0 ? 'swiping-up' : 'swiping', + swipeDistance: Math.abs(deltaY), + swipeStyle: `--swipe-distance: ${Math.abs(deltaY)}rpx;` + }); + e.stopPropagation(); + } + }, + + onTouchEnd(e) { + if (!this.properties.swipeToClose || !this.data.isSwiping) { + this.restartAutoHide(); + return; + } + const deltaY = this.data.currentY - this.data.touchStartY; + const absDeltaY = Math.abs(deltaY); + if (absDeltaY > this.properties.swipeThreshold) { + this.hideNotification(); + } else { + this.setData({ + swipingClass: '', + swipeStyle: 'transition: transform 0.3s ease; transform: translateY(0);' + }); + setTimeout(() => { + this.setData({ + swipingClass: '', + swipeStyle: '' + }); + }, 300); + this.restartAutoHide(); + } + this.setData({ isSwiping: false }); + }, + + restartAutoHide() { + const remainingTime = (this.data.progress / 100) * this.properties.duration; + if (remainingTime > 0) { + this.data.autoHideTimeout = setTimeout(() => { + this.hideNotification(); + }, remainingTime); + } + }, + + onTap() { + const { notificationData } = this.data; + if (!notificationData) return; + this.triggerEvent('tap', notificationData); + this.hideNotification(); + this.navigateToChatPage(notificationData); + }, + + onClose(e) { + e.stopPropagation(); + this.triggerEvent('close'); + this.hideNotification(); + }, + + onMute(e) { + e.stopPropagation(); + const newMutedState = !this.data.isMuted; + this.setData({ isMuted: newMutedState }); + this.saveMuteSetting(newMutedState); + + const app = getApp(); + if (app && app.toggleDoNotDisturb) { + app.toggleDoNotDisturb(newMutedState); + } + this.triggerEvent('mute', { muted: newMutedState }); + wx.showToast({ + title: newMutedState ? '已开启免打扰' : '已关闭免打扰', + icon: 'success', + duration: 1500 + }); + + if (newMutedState) { + this.hideNotification(); + } + }, + + formatTime(timestamp) { + if (!timestamp) return ''; + const date = new Date(timestamp); + const now = new Date(); + const diff = now - date; + if (diff < 60 * 1000) return '刚刚'; + if (diff < 60 * 60 * 1000) return Math.floor(diff / (60 * 1000)) + '分钟前'; + if (date.toDateString() === now.toDateString()) { + return date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0'); + } + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + if (date.toDateString() === yesterday.toDateString()) { + return '昨天 ' + date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0'); + } + return (date.getMonth() + 1) + '月' + date.getDate() + '日'; + }, + + navigateToChatPage(data) { + if (!data || !data.message) { + console.warn('通知数据无效,无法跳转'); + return; + } + + const msg = data.message; + let path = ''; + let queryParam = null; + + // 根据 notificationType 或消息字段判断 + if (data.notificationType === 'cs' || msg.teamId) { + // 客服消息 + const teamId = data.teamId || msg.teamId; + path = '/pages/kefuliaotian/kefuliaotian'; + queryParam = { teamId: teamId }; + } else if (data.notificationType === 'group' || msg.groupId) { + // 群聊消息 + const groupId = data.groupId || msg.groupId; + const orderId = data.orderId || msg.orderId || ''; + const groupName = data.groupName || '订单群聊'; + const groupAvatar = data.groupAvatar || ''; + const isCross = data.isCross || msg.isCross || 0; + path = '/pages/qunliaotian/qunliaotian'; + queryParam = { + groupId, + orderId, + groupName, + groupAvatar, + isCross + }; + } else { + // 默认私聊 + const toUserId = data.senderId || msg.senderId; + const toName = data.senderName || '用户'; + const toAvatar = data.avatar || ''; + path = '/pages/liaotian/liaotian'; + queryParam = { + toUserId, + toName, + toAvatar + }; + } + + if (path && queryParam) { + const encoded = encodeURIComponent(JSON.stringify(queryParam)); + wx.navigateTo({ + url: `${path}?data=${encoded}`, + fail: (err) => { + console.error('通知跳转失败:', err); + wx.switchTab({ url: '/pages/xiaoxi/xiaoxi' }); + } + }); + } else { + wx.switchTab({ url: '/pages/xiaoxi/xiaoxi' }); + } + } + } + }); \ No newline at end of file diff --git a/components/global-notification/global-notification.json b/components/global-notification/global-notification.json new file mode 100644 index 0000000..7e37c03 --- /dev/null +++ b/components/global-notification/global-notification.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} \ No newline at end of file diff --git a/components/global-notification/global-notification.wxml b/components/global-notification/global-notification.wxml new file mode 100644 index 0000000..d76b58c --- /dev/null +++ b/components/global-notification/global-notification.wxml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + {{title}} + {{message}} + + {{formatTime(notificationData ? notificationData.timestamp : '')}} + + + + + + + + × + + + + + + {{isMuted ? '🔕' : '🔔'}} + + + + + + + + + + \ No newline at end of file diff --git a/components/global-notification/global-notification.wxss b/components/global-notification/global-notification.wxss new file mode 100644 index 0000000..fc67be6 --- /dev/null +++ b/components/global-notification/global-notification.wxss @@ -0,0 +1,215 @@ +.global-notification { + position: fixed; + left: 20rpx; + right: 20rpx; + top: 0rpx; + z-index: 99999; + opacity: 0; + transform: translateY(-120%); + transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); + pointer-events: none; + overflow: hidden; + border-radius: 24rpx; + background: linear-gradient(135deg, + rgba(41, 47, 61, 0.98) 0%, + rgba(31, 36, 48, 0.98) 100%); + backdrop-filter: blur(30rpx) saturate(180%); + -webkit-backdrop-filter: blur(30rpx) saturate(180%); + border: 1rpx solid rgba(255, 255, 255, 0.08); + box-shadow: + 0 20rpx 60rpx rgba(0, 0, 0, 0.4), + 0 0 0 1rpx rgba(255, 255, 255, 0.03), + inset 0 1rpx 0 rgba(255, 255, 255, 0.1); + min-height: 140rpx; + touch-action: pan-y; +} + +.global-notification.show { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} + +.notification-swipe-indicator { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4rpx; + display: flex; + justify-content: center; + align-items: center; + z-index: 10; +} + +.swipe-line { + width: 40rpx; + height: 4rpx; + background: rgba(255, 255, 255, 0.3); + border-radius: 2rpx; + transition: all 0.3s; +} + +.notification-swipe-indicator.active .swipe-line { + background: rgba(255, 255, 255, 0.6); + transform: scaleX(1.2); +} + +.notification-content { + display: flex; + align-items: center; + padding: 32rpx; + min-height: 140rpx; + position: relative; +} + +.notification-avatar { + width: 88rpx; + height: 88rpx; + border-radius: 44rpx; + margin-right: 24rpx; + flex-shrink: 0; + border: 3rpx solid rgba(212, 175, 55, 0.6); + box-shadow: + 0 8rpx 24rpx rgba(212, 175, 55, 0.2), + inset 0 1rpx 0 rgba(255, 255, 255, 0.3); + background-color: rgba(0, 0, 0, 0.2); +} + +.notification-body { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + justify-content: center; +} + +.notification-title { + font-size: 34rpx; + font-weight: 600; + margin-bottom: 8rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + background: linear-gradient(135deg, #D4AF37 0%, #FFD700 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3); +} + +.notification-message { + font-size: 28rpx; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + color: rgba(255, 255, 255, 0.95); + text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.5); +} + +.notification-time { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.5); + margin-top: 4rpx; + font-weight: 300; +} + +.notification-actions { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-left: 20rpx; + gap: 16rpx; +} + +.notification-close, +.notification-mute { + width: 56rpx; + height: 56rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 28rpx; + background: rgba(255, 255, 255, 0.08); + border: 1rpx solid rgba(255, 255, 255, 0.1); + transition: all 0.3s; + flex-shrink: 0; + box-shadow: + 0 4rpx 12rpx rgba(0, 0, 0, 0.2), + inset 0 1rpx 0 rgba(255, 255, 255, 0.05); +} + +.notification-close:active, +.notification-mute:active { + background: rgba(255, 255, 255, 0.15); + transform: scale(0.92); +} + +.close-icon { + font-size: 32rpx; + font-weight: 300; + color: rgba(255, 255, 255, 0.9); + line-height: 1; +} + +.mute-icon { + font-size: 28rpx; + line-height: 0; + color: rgba(255, 255, 255, 0.9); +} + +.notification-mute.muted .mute-icon { + color: #D4AF37; +} + +.notification-progress { + height: 6rpx; + background: rgba(255, 255, 255, 0.05); + overflow: hidden; + border-radius: 0 0 24rpx 24rpx; +} + +.progress-bar { + height: 100%; + width: 100%; + background: linear-gradient(90deg, + #D4AF37 0%, + #FFD700 50%, + #D4AF37 100%); + background-size: 200% 100%; + transition: width 0.1s linear; + box-shadow: 0 0 20rpx rgba(212, 175, 55, 0.4); + animation: progressShine 2s linear infinite; +} + +@keyframes progressShine { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +.progress-bar.warning { + background: linear-gradient(90deg, + #ff3b30 0%, + #ff6b6b 50%, + #ff3b30 100%); + animation: progressShine 1s linear infinite; +} + +.global-notification.swiping { + opacity: 0.7; + transform: translateY(calc(var(--swipe-distance, 0) * -1)); +} + +.global-notification.swiping-up { + opacity: 0.3; + transform: translateY(calc(var(--swipe-distance, 0) * -1)); +} + +.global-notification:active:not(.swiping) { + transform: scale(0.995) translateY(0); + transition: transform 0.1s; +} \ No newline at end of file diff --git a/components/navigation-bar/navigation-bar.js b/components/navigation-bar/navigation-bar.js new file mode 100644 index 0000000..eb1770e --- /dev/null +++ b/components/navigation-bar/navigation-bar.js @@ -0,0 +1,102 @@ +Component({ + options: { + multipleSlots: true // 在组件定义时的选项中启用多slot支持 + }, + /** + * 组件的属性列表 + */ + properties: { + extClass: { + type: String, + value: '' + }, + title: { + type: String, + value: '' + }, + background: { + type: String, + value: '' + }, + color: { + type: String, + value: '' + }, + back: { + type: Boolean, + value: true + }, + loading: { + type: Boolean, + value: false + }, + homeButton: { + type: Boolean, + value: false, + }, + animated: { + // 显示隐藏的时候opacity动画效果 + type: Boolean, + value: true + }, + show: { + // 显示隐藏导航,隐藏的时候navigation-bar的高度占位还在 + type: Boolean, + value: true, + observer: '_showChange' + }, + // back为true的时候,返回的页面深度 + delta: { + type: Number, + value: 1 + }, + }, + /** + * 组件的初始数据 + */ + data: { + displayStyle: '' + }, + lifetimes: { + attached() { + const rect = wx.getMenuButtonBoundingClientRect() + const platform = (wx.getDeviceInfo() || wx.getSystemInfoSync()).platform + const isAndroid = platform === 'android' + const isDevtools = platform === 'devtools' + const { windowWidth, safeArea: { top = 0, bottom = 0 } = {} } = wx.getWindowInfo() || wx.getSystemInfoSync() + this.setData({ + ios: !isAndroid, + innerPaddingRight: `padding-right: ${windowWidth - rect.left}px`, + leftWidth: `width: ${windowWidth - rect.left}px`, + safeAreaTop: isDevtools || isAndroid ? `height: calc(var(--height) + ${top}px); padding-top: ${top}px` : `` + }) + }, + }, + /** + * 组件的方法列表 + */ + methods: { + _showChange(show) { + const animated = this.data.animated + let displayStyle = '' + if (animated) { + displayStyle = `opacity: ${show ? '1' : '0' + };transition:opacity 0.5s;` + } else { + displayStyle = `display: ${show ? '' : 'none'}` + } + this.setData({ + displayStyle + }) + }, + back() { + const data = this.data + if (data.delta) { + wx.navigateBack({ + delta: data.delta + }) + } + this.triggerEvent('back', { delta: data.delta }, {}) + } + }, +}) diff --git a/components/navigation-bar/navigation-bar.json b/components/navigation-bar/navigation-bar.json new file mode 100644 index 0000000..4a20f17 --- /dev/null +++ b/components/navigation-bar/navigation-bar.json @@ -0,0 +1,5 @@ +{ + "component": true, + "styleIsolation": "apply-shared", + "usingComponents": {} +} \ No newline at end of file diff --git a/components/navigation-bar/navigation-bar.wxml b/components/navigation-bar/navigation-bar.wxml new file mode 100644 index 0000000..2bfe519 --- /dev/null +++ b/components/navigation-bar/navigation-bar.wxml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{title}} + + + + + + + + + + + + diff --git a/components/navigation-bar/navigation-bar.wxss b/components/navigation-bar/navigation-bar.wxss new file mode 100644 index 0000000..8bd379e --- /dev/null +++ b/components/navigation-bar/navigation-bar.wxss @@ -0,0 +1,96 @@ +.weui-navigation-bar { + --weui-FG-0:rgba(0,0,0,.9); + --height: 44px; + --left: 16px; +} +.weui-navigation-bar .android { + --height: 48px; +} + +.weui-navigation-bar { + overflow: hidden; + color: var(--weui-FG-0); + flex: none; +} + +.weui-navigation-bar__inner { + position: relative; + top: 0; + left: 0; + height: calc(var(--height) + env(safe-area-inset-top)); + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding-top: env(safe-area-inset-top); + width: 100%; + box-sizing: border-box; +} + +.weui-navigation-bar__left { + position: relative; + padding-left: var(--left); + display: flex; + flex-direction: row; + align-items: flex-start; + height: 100%; + box-sizing: border-box; +} + +.weui-navigation-bar__btn_goback_wrapper { + padding: 11px 18px 11px 16px; + margin: -11px -18px -11px -16px; +} + +.weui-navigation-bar__btn_goback_wrapper.weui-active { + opacity: 0.5; +} + +.weui-navigation-bar__btn_goback { + font-size: 12px; + width: 12px; + height: 24px; + -webkit-mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%; + mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%; + -webkit-mask-size: cover; + mask-size: cover; + background-color: var(--weui-FG-0); +} + +.weui-navigation-bar__center { + font-size: 17px; + text-align: center; + position: relative; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + font-weight: bold; + flex: 1; + height: 100%; +} + +.weui-navigation-bar__loading { + margin-right: 4px; + align-items: center; +} + +.weui-loading { + font-size: 16px; + width: 16px; + height: 16px; + display: block; + background: transparent url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='80px' height='80px' viewBox='0 0 80 80' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3Eloading%3C/title%3E%3Cdefs%3E%3ClinearGradient x1='94.0869141%25' y1='0%25' x2='94.0869141%25' y2='90.559082%25' id='linearGradient-1'%3E%3Cstop stop-color='%23606060' stop-opacity='0' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3ClinearGradient x1='100%25' y1='8.67370605%25' x2='100%25' y2='90.6286621%25' id='linearGradient-2'%3E%3Cstop stop-color='%23606060' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3C/defs%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' opacity='0.9'%3E%3Cg%3E%3Cpath d='M40,0 C62.09139,0 80,17.90861 80,40 C80,62.09139 62.09139,80 40,80 L40,73 C58.2253967,73 73,58.2253967 73,40 C73,21.7746033 58.2253967,7 40,7 L40,0 Z' fill='url(%23linearGradient-1)'%3E%3C/path%3E%3Cpath d='M40,0 L40,7 C21.7746033,7 7,21.7746033 7,40 C7,58.2253967 21.7746033,73 40,73 L40,80 C17.90861,80 0,62.09139 0,40 C0,17.90861 17.90861,0 40,0 Z' fill='url(%23linearGradient-2)'%3E%3C/path%3E%3Ccircle id='Oval' fill='%23606060' cx='40.5' cy='3.5' r='3.5'%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A") no-repeat; + background-size: 100%; + margin-left: 0; + animation: loading linear infinite 1s; +} + +@keyframes loading { + from { + transform: rotate(0); + } + to { + transform: rotate(360deg); + } +} diff --git a/components/order-card/order-card.js b/components/order-card/order-card.js new file mode 100644 index 0000000..b94bb6c --- /dev/null +++ b/components/order-card/order-card.js @@ -0,0 +1,102 @@ +const app = getApp(); + +Component({ + properties: { + orderId: { type: String, value: '' } + }, + + data: { + order: null, + loading: false, + errorMsg: '', + // 跨平台字段 + isCross: false, // 是否跨平台订单 + dispatchType: 0, // 1-我方派单,2-对方派单 + partnerClubId: '', // 对方俱乐部ID + }, + + lifetimes: { + attached() { + if (this.properties.orderId) { + this.fetchOrderDetail(); + } + } + }, + + methods: { + // 获取订单详情(含跨平台字段) + fetchOrderDetail() { + if (!this.properties.orderId) return; + this.setData({ loading: true, errorMsg: '' }); + const baseUrl = app.globalData.apiBaseUrl; + wx.request({ + url: `${baseUrl}/dingdan/order_detail/`, // 需后端返回 is_cross, dispatch_type, partner_club_id + method: 'POST', + data: { order_id: this.properties.orderId }, + header: { 'content-type': 'application/json' }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 0) { + const order = res.data.data; + // 处理图片URL + if (order.tupian && !order.tupian.startsWith('http')) { + order.tupian = app.globalData.ossImageUrl + order.tupian; + } + this.setData({ + order, + loading: false, + isCross: order.is_cross === 1, + dispatchType: order.dispatch_type || 0, + partnerClubId: order.partner_club_id || '', + }); + // 通知父页面状态 + this.triggerEvent('statusready', { + isCross: this.data.isCross, + dispatchType: this.data.dispatchType, + partnerClubId: this.data.partnerClubId + }); + } else { + this.setData({ errorMsg: res.data?.msg || '获取订单失败', loading: false }); + } + }, + fail: () => { + this.setData({ errorMsg: '网络请求失败', loading: false }); + } + }); + }, + + // 发送订单卡片消息(原有) + sendOrderMessage() { + if (!this.data.order) return; + this.triggerEvent('sendorder', { order: this.data.order }); + }, + + // 跨平台消息转发(供父页面调用) + forwardMessage(payload) { + const that = this; + const baseUrl = app.globalData.apiBaseUrl; + return new Promise((resolve, reject) => { + wx.request({ + url: `${baseUrl}/dingdan/kptxxts`, + method: 'POST', + data: { + order_id: that.properties.orderId, + message_type: payload.type || 'text', + content: payload.content || '', + image_url: payload.imageUrl || '', // 图片等 + }, + header: { 'content-type': 'application/json' }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 0) { + resolve(res.data); + } else { + reject(res.data); + } + }, + fail: (err) => { + reject(err); + } + }); + }); + } + } +}); \ No newline at end of file diff --git a/components/order-card/order-card.json b/components/order-card/order-card.json new file mode 100644 index 0000000..7e37c03 --- /dev/null +++ b/components/order-card/order-card.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} \ No newline at end of file diff --git a/components/order-card/order-card.wxml b/components/order-card/order-card.wxml new file mode 100644 index 0000000..c87ad17 --- /dev/null +++ b/components/order-card/order-card.wxml @@ -0,0 +1,20 @@ + + + 关联订单 + {{order.dingdan_id}} + + 跨平台 + + + + + {{order.jieshao || '暂无介绍'}} + ¥{{order.jine}} + + 发送 + + {{errorMsg}} + + + 加载订单信息... + \ No newline at end of file diff --git a/components/order-card/order-card.wxss b/components/order-card/order-card.wxss new file mode 100644 index 0000000..62c2737 --- /dev/null +++ b/components/order-card/order-card.wxss @@ -0,0 +1,39 @@ +.order-card { + background: #fff; + margin: 15rpx 20rpx; + border-radius: 12rpx; + overflow: hidden; + box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05); +} +.card-header { + display: flex; + align-items: center; + padding: 16rpx 20rpx; + border-bottom: 1rpx solid #f0f0f0; +} +.card-title { font-size: 26rpx; color: #666; } +.card-id { font-size: 24rpx; color: #999; margin-left: 15rpx; } +.cross-tag { + margin-left: auto; + background: #ff9900; + color: #fff; + font-size: 20rpx; + padding: 2rpx 12rpx; + border-radius: 8rpx; +} +.card-body { + display: flex; + align-items: center; + padding: 20rpx; +} +.card-img { width: 100rpx; height: 100rpx; border-radius: 8rpx; margin-right: 15rpx; } +.card-info { flex: 1; } +.card-jieshao { font-size: 28rpx; color: #333; } +.card-jine { font-size: 26rpx; color: #e74c3c; } +.card-send-btn { + width: 80rpx; height: 50rpx; line-height: 50rpx; + text-align: center; background: #07c160; color: #fff; + border-radius: 8rpx; font-size: 24rpx; +} +.card-error { padding: 20rpx; color: #e74c3c; text-align: center; } +.loading-text { display: block; padding: 20rpx; text-align: center; color: #999; } \ No newline at end of file diff --git a/components/order-sender/order-sender.js b/components/order-sender/order-sender.js new file mode 100644 index 0000000..f4c9444 --- /dev/null +++ b/components/order-sender/order-sender.js @@ -0,0 +1,16 @@ +const app = getApp(); + +Component({ + properties: { + visible: Boolean, + showDetail: { type: Boolean, value: false } + }, + + data: {}, + + methods: { + close() { + this.triggerEvent('close'); + } + } +}); \ No newline at end of file diff --git a/components/order-sender/order-sender.json b/components/order-sender/order-sender.json new file mode 100644 index 0000000..62389a9 --- /dev/null +++ b/components/order-sender/order-sender.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} + } \ No newline at end of file diff --git a/components/order-sender/order-sender.wxml b/components/order-sender/order-sender.wxml new file mode 100644 index 0000000..ae53ed5 --- /dev/null +++ b/components/order-sender/order-sender.wxml @@ -0,0 +1,12 @@ + + + + 提示 + + + + 📋 + 此功能暂未开放 + 敬请期待 + + \ No newline at end of file diff --git a/components/order-sender/order-sender.wxss b/components/order-sender/order-sender.wxss new file mode 100644 index 0000000..c0ffc48 --- /dev/null +++ b/components/order-sender/order-sender.wxss @@ -0,0 +1,45 @@ +.order-sender-mask { + position: fixed; top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.5); + z-index: 2000; + } + .order-sender { + position: fixed; bottom: 0; left: 0; right: 0; + height: 360rpx; + background: #fff; + border-radius: 24rpx 24rpx 0 0; + z-index: 2001; + transform: translateY(100%); + transition: 0.25s; + display: flex; + flex-direction: column; + } + .order-sender.show { transform: translateY(0); } + + .header { + display: flex; justify-content: space-between; + padding: 24rpx 30rpx; + border-bottom: 1rpx solid #f0f0f0; + } + .title { font-size: 32rpx; font-weight: 600; } + .close { font-size: 44rpx; color: #999; padding: 0 12rpx; } + + .content { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-bottom: 40rpx; + } + .icon { font-size: 80rpx; margin-bottom: 20rpx; } + .notice { + font-size: 34rpx; + font-weight: 600; + color: #333; + margin-bottom: 10rpx; + } + .sub { + font-size: 24rpx; + color: #999; + } \ No newline at end of file diff --git a/components/popup-notice/popup-notice.js b/components/popup-notice/popup-notice.js new file mode 100644 index 0000000..64589db --- /dev/null +++ b/components/popup-notice/popup-notice.js @@ -0,0 +1,324 @@ +// components/popup-notice/popup-notice.js +import PopupService from '../../services/popupService.js'; + +Component({ + data: { + // 原有全屏弹窗数据 + visible: false, + title: '', + images: [], + processedImages: [], + hasImages: false, + hasTextOnly: false, + textContent: '', + duration: 0, + remainingSeconds: 0, + muteChecked: false, + showMuteCheckbox: false, + isFullscreen: false, + timer: null, + + // 悬浮窗相关数据 + isMinimized: false, + floatX: 20, + floatY: 200, + savedRemainingSeconds: 0, + floatAvatarUrl: '', + + // 🆕 自定义预览数据 + showCustomPreview: false, // 是否显示自定义预览遮罩 + previewUrls: [], // 当前预览的图片URL列表 + currentPreviewIndex: 0, // 当前预览图片索引 + }, + + lifetimes: { + detached() { + this.clearTimer(); + } + }, + + methods: { + // ========== 原有所有方法(完全不变,一字不漏) ========== + async show(config, onCloseCallback) { + const { title, images, duration, showMuteCheckbox, serverTime } = config; + this.onCloseCallback = onCloseCallback; + + let safeImages = JSON.parse(JSON.stringify(images || [])); + const hasImages = safeImages.length > 0; + let hasTextOnly = false, textContent = ''; + if (!hasImages) { + hasTextOnly = true; + textContent = config.text || '暂无内容'; + } + + this.setData({ + visible: true, + isMinimized: false, + title: title || '', + images: safeImages, + hasImages, + hasTextOnly, + textContent, + duration: duration || 0, + remainingSeconds: duration || 0, + muteChecked: false, + showMuteCheckbox: showMuteCheckbox || false, + isFullscreen: false, + processedImages: [], + }); + + if (duration > 0) { + this.startCountdown(); + } + + if (hasImages) { + const processed = []; + const screenWidth = wx.getSystemInfoSync().windowWidth; + const containerWidth = screenWidth * 0.9; + const imageWidth = containerWidth - 60; + for (let i = 0; i < safeImages.length; i++) { + const img = safeImages[i]; + const fullUrl = this.getFullImageUrl(img.url); + let imageHeight = 200; + try { + const res = await this.getImageInfoPromise(fullUrl); + const realHeight = (imageWidth * res.height) / res.width; + imageHeight = realHeight; + } catch (e) { + console.error('获取图片信息失败', fullUrl, e); + } + processed.push({ + url: fullUrl, + text: img.text, + height: imageHeight, + }); + } + this.setData({ processedImages: processed }); + } + }, + + getImageInfoPromise(url) { + return new Promise((resolve, reject) => { + wx.getImageInfo({ + src: url, + success: resolve, + fail: reject, + }); + }); + }, + + hide(muteToday = false) { + this.clearTimer(); + this.setData({ + visible: false, + isMinimized: false + }); + if (this.onCloseCallback) { + this.onCloseCallback({ muteToday }); + } + }, + + startCountdown() { + this.clearTimer(); + this.timer = setInterval(() => { + let sec = this.data.remainingSeconds; + if (sec <= 1) { + this.clearTimer(); + this.setData({ remainingSeconds: 0 }); + } else { + this.setData({ remainingSeconds: sec - 1 }); + } + }, 1000); + }, + + clearTimer() { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + }, + + onCloseTap() { + if (this.data.remainingSeconds > 0) { + wx.showToast({ + title: `请阅读${this.data.remainingSeconds}秒后再关闭`, + icon: 'none' + }); + return; + } + if (this.data.showMuteCheckbox && this.data.muteChecked) { + wx.showModal({ + title: '提示', + content: '今天将不再收到本页面的任何公告,确认吗?', + success: (res) => { + if (res.confirm) this.hide(true); + }, + }); + } else { + this.hide(false); + } + }, + + onMuteChange(e) { + this.setData({ muteChecked: e.detail.value.length > 0 }); + }, + + // 🆕 替换原有的 onPreviewImage,改用自定义预览(不触发 onShow) + onCustomPreview(e) { + const index = e.currentTarget.dataset.index; + const urls = this.data.processedImages.map(img => img.url); + this.setData({ + showCustomPreview: true, + previewUrls: urls, + currentPreviewIndex: index !== undefined ? index : 0 + }); + }, + + // 🆕 关闭自定义预览 + onCloseCustomPreview() { + this.setData({ + showCustomPreview: false, + previewUrls: [], + currentPreviewIndex: 0 + }); + }, + + // 🆕 swiper 切换事件 + onPreviewSwiperChange(e) { + this.setData({ + currentPreviewIndex: e.detail.current + }); + }, + + // 保留原 onPreviewImage 方法以防外部调用,但不再使用 + onPreviewImage(e) { + // 重定向到自定义预览 + this.onCustomPreview(e); + }, + + onImageError(e) { + console.error('图片加载失败:', e.currentTarget.dataset.url); + }, + + toggleFullscreen() { + this.setData({ isFullscreen: !this.data.isFullscreen }); + }, + + getFullImageUrl(relativeUrl) { + if (!relativeUrl) return ''; + if (relativeUrl.startsWith('http')) return relativeUrl; + const app = getApp(); + let base = app.globalData.ossImageUrl || ''; + base = base.replace(/\/+$/, ''); + const clean = relativeUrl.replace(/^\/+/, ''); + return base ? base + '/' + clean : clean; + }, + + preventMove() {}, + + onMinimize() { + const remaining = this.data.remainingSeconds; + this.clearTimer(); + + const app = getApp(); + let avatarUrl = ''; + if (app.globalData.morentouxiang) { + avatarUrl = app.globalData.morentouxiang; + } else { + avatarUrl = '/images/default-avatar.png'; + } + + if (avatarUrl && !avatarUrl.startsWith('http')) { + const ossBase = app.globalData.ossImageUrl || ''; + avatarUrl = ossBase.replace(/\/+$/, '') + '/' + avatarUrl.replace(/^\/+/, ''); + } + + this.setData({ + visible: false, + isMinimized: true, + savedRemainingSeconds: remaining, + floatX: 20, + floatY: 200, + floatAvatarUrl: avatarUrl + }); + }, + + onFloatChange(e) { + this.setData({ + floatX: e.detail.x, + floatY: e.detail.y + }); + }, + + onFloatTap() { + this.setData({ + visible: true, + isMinimized: false, + remainingSeconds: this.data.savedRemainingSeconds + }); + + if (this.data.duration > 0 && this.data.savedRemainingSeconds > 0) { + this.startCountdown(); + } + }, + + onFloatAvatarError() { + this.setData({ + floatAvatarUrl: '/images/avatar-placeholder.png' + }); + }, + + cleanup() { + this.clearTimer(); + this.setData({ + visible: false, + isMinimized: false + }); + PopupService.reset(); + }, + + onFloatTouchStart(e) { + const touch = e.touches[0]; + this.touchStartX = touch.clientX; + this.touchStartY = touch.clientY; + this.startFloatX = this.data.floatX; + this.startFloatY = this.data.floatY; + this.hasMoved = false; + return false; + }, + + onFloatTouchMove(e) { + const touch = e.touches[0]; + const deltaX = touch.clientX - this.touchStartX; + const deltaY = touch.clientY - this.touchStartY; + + if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { + this.hasMoved = true; + } + + let newX = this.startFloatX + deltaX; + let newY = this.startFloatY + deltaY; + + const systemInfo = wx.getSystemInfoSync(); + const screenWidth = systemInfo.windowWidth; + const screenHeight = systemInfo.windowHeight; + const estimatedSize = 60; + + newX = Math.max(0, Math.min(newX, screenWidth - estimatedSize)); + newY = Math.max(0, Math.min(newY, screenHeight - estimatedSize)); + this.setData({ floatX: newX, floatY: newY }); + + return false; + }, + + onFloatTouchEnd(e) { + if (!this.hasMoved) { + this.onFloatTap(); + } + return false; + }, + + // 🆕 阻止事件冒泡(用于预览遮罩内部点击不关闭) + stopPropagation() {} + } + }); \ No newline at end of file diff --git a/components/popup-notice/popup-notice.json b/components/popup-notice/popup-notice.json new file mode 100644 index 0000000..62389a9 --- /dev/null +++ b/components/popup-notice/popup-notice.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} + } \ No newline at end of file diff --git a/components/popup-notice/popup-notice.wxml b/components/popup-notice/popup-notice.wxml new file mode 100644 index 0000000..8e682cb --- /dev/null +++ b/components/popup-notice/popup-notice.wxml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + {{isFullscreen ? '✕' : '⛶'}} + + + + + {{title}} + + + + + {{textContent}} + + + {{item.text}} + + + + + + + + + + + + 请阅读 {{remainingSeconds}} 秒后关闭 + + + + + 今日不再提示 + + + + + {{remainingSeconds > 0 ? '阅读中...' : '关闭'}} + + + + + + + + + + + + + + + {{currentPreviewIndex + 1}} / {{previewUrls.length}} + × + + + + + + + \ No newline at end of file diff --git a/components/popup-notice/popup-notice.wxss b/components/popup-notice/popup-notice.wxss new file mode 100644 index 0000000..ee4873b --- /dev/null +++ b/components/popup-notice/popup-notice.wxss @@ -0,0 +1,295 @@ +/* components/popup-notice/popup-notice.wxss */ + +/* ========== 全屏弹窗样式(原有,完全不变) ========== */ +.popup-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; +} + +.popup-container { + width: 90%; + height: 85vh; + background: white; + border-radius: 32rpx; + display: flex; + flex-direction: column; + overflow: hidden; + position: relative; +} + +.popup-container.fullscreen { + width: 100%; + height: 100%; + border-radius: 0; +} + +.action-buttons { + position: absolute; + top: 20rpx; + right: 20rpx; + display: flex; + gap: 16rpx; + z-index: 10; +} + +.minimize-btn, +.fullscreen-btn { + width: 60rpx; + height: 60rpx; + background: rgba(0, 0, 0, 0.4); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(4rpx); + transition: all 0.2s; +} + +.minimize-btn:active, +.fullscreen-btn:active { + background: rgba(0, 0, 0, 0.6); + transform: scale(0.9); +} + +.btn-icon { + font-size: 36rpx; + color: white; + font-weight: bold; + line-height: 1; +} + +.popup-title { + font-size: 40rpx; + font-weight: bold; + color: #000; + text-align: center; + padding: 30rpx 30rpx 20rpx; + border-bottom: 2rpx solid #eee; + background: white; +} + +.scroll-wrapper { + flex: 1; + overflow: hidden; + width: 100%; +} + +.scroll-view { + height: 100%; + width: 100%; + padding: 0; +} + +.text-content { + font-size: 36rpx; + font-weight: bold; + color: #000; + line-height: 1.6; + padding: 30rpx; + text-align: left; +} + +.image-block { + display: flex; + flex-direction: column; + width: 100%; + margin: 0; + padding: 0; +} + +.block-text { + font-size: 36rpx; + font-weight: bold; + color: #000; + padding: 16rpx 30rpx 4rpx 30rpx; + text-align: left; + line-height: 1.4; +} + +.image-padding { + padding: 0 30rpx; + box-sizing: border-box; + width: 100%; +} + +.block-image { + width: 100%; + display: block; + background: #f5f5f5; + border-radius: 16rpx; +} + +.bottom-fixed { + background: white; + border-top: 1rpx solid #eee; + padding: 20rpx 0 30rpx 0; + width: 100%; + box-sizing: border-box; +} + +.countdown-tip { + text-align: center; + font-size: 28rpx; + color: #ff6600; + margin-bottom: 20rpx; +} + +.mute-row { + display: flex; + justify-content: center; + margin-bottom: 20rpx; +} + +.mute-label { + font-size: 28rpx; + color: #555; + margin-left: 12rpx; +} + +/* 🔥 关闭按钮(替换原有 button,增加扫光) */ +.close-btn { + width: 600rpx; + height: 100rpx; + background: #1a1a1a; + border-radius: 60rpx; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + margin: 0 auto; +} + +.close-btn:active { + opacity: 0.85; + transform: scale(0.98); +} + +.close-btn-disabled { + opacity: 0.6; + pointer-events: none; +} + +.close-btn-text { + color: white; + font-size: 40rpx; + font-weight: bold; + position: relative; + z-index: 2; +} + +.close-btn-sweep { + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5), transparent); + transform: skewX(-20deg); + animation: sweep 2.5s infinite ease-in-out; + z-index: 1; + pointer-events: none; +} + +@keyframes sweep { + 0% { left: -100%; } + 40% { left: 100%; } + 100% { left: 100%; } +} + +/* ========== 悬浮窗样式(纯圆形,适配自定义触摸拖动) ========== */ +.floating-avatar-wrapper { + position: fixed; + width: 120rpx; + height: 120rpx; + z-index: 10001; + transform: translateZ(0); + will-change: left, top; + display: flex; + align-items: center; + justify-content: center; +} + +.floating-avatar { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.3); + border: 2rpx solid rgba(255, 215, 0, 0.6); + background: #f0f0f0; + display: block; + transform: translateZ(0); +} + +/* 以下原有 movable-view 相关样式已弃用,但保留以维持代码完整(用户要求不删代码) */ +.floating-area { + display: none; +} +.floating-view { + display: none; +} +.float-avatar-wrapper { + display: none; +} +.float-text { + display: none; +} + +/* ========== 自定义图片预览遮罩样式(全屏,不触发 onShow) ========== */ +.custom-preview-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.95); + z-index: 100000; + display: flex; + flex-direction: column; +} + +.preview-header { + height: 100rpx; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 30rpx; + color: white; + font-size: 32rpx; + background: rgba(0, 0, 0, 0.3); +} + +.preview-index { + color: white; +} + +.preview-close { + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 48rpx; + color: white; + background: rgba(255, 255, 255, 0.2); + border-radius: 50%; +} + +.preview-swiper { + flex: 1; + width: 100%; + height: 100%; +} + +.preview-image { + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/config/roles.js b/config/roles.js new file mode 100644 index 0000000..95c04dc --- /dev/null +++ b/config/roles.js @@ -0,0 +1,144 @@ +/* + * 角色配置表 —— 用于动态切换底部 TabBar 展示内容 + * 每个角色的 tabList 数组元素顺序决定 TabBar 显示顺序 + * 字段说明: + * pagePath : 页面路径(以 pages/ 开头) + * text : TabBar 文字 + * iconPath : 未选中图标(相对路径) + * selectedIconPath: 选中图标 + */ +const roles = { + // 普通老板(默认) + normal: { + name: '普通老板', + defaultPage: 'pages/index/index', + tabList: [ + { + pagePath: 'pages/index/index', + text: '商城', + iconPath: '/images/zhuye.png', + selectedIconPath: '/images/zhuye.png' + }, + { + pagePath: 'pages/fenlei/fenlei', + text: '分类', + iconPath: '/images/fenlei.png', + selectedIconPath: '/images/fenlei.png' + }, + { + pagePath: 'pages/xiaoxi/xiaoxi', + text: '消息', + iconPath: '/images/xiaoxi.png', + selectedIconPath: '/images/xiaoxi.png' + }, + { + pagePath: 'pages/wode/wode', + text: '我的', + iconPath: '/images/wode.png', + selectedIconPath: '/images/wode.png' + } + ] + }, + // 打手 + dashou: { + name: '打手', + defaultPage: 'pages/jiedanchi/jiedanchi', + tabList: [ + { + pagePath: 'pages/jiedanchi/jiedanchi', + text: '接单池', + iconPath: '/images/jiedanchi.png', + selectedIconPath: '/images/jiedanchi.png' + }, + { + pagePath: 'pages/xiaoxi/xiaoxi', + text: '消息', + iconPath: '/images/xiaoxi.png', + selectedIconPath: '/images/xiaoxi.png' + }, + { + pagePath: 'pages/dashoudingdan/dashoudingdan', + text: '订单', + iconPath: '/images/dashoudingdan_icon.png', // 请替换为实际图标 + selectedIconPath: '/images/dashoudingdan_icon.png' + }, + { + pagePath: 'pages/dashouduan/dashouduan', + text: '我的', + iconPath: '/images/dashouduan_icon.png', + selectedIconPath: '/images/dashouduan_icon.png' + } + ] + }, + // 商家 + shangjia: { + name: '商家', + defaultPage: 'pages/index/index', + tabList: [ + { + pagePath: 'pages/index/index', + text: '首页', + iconPath: '/images/zhuye.png', + selectedIconPath: '/images/zhuye.png' + }, + { + pagePath: 'pages/sjdingdan/sjdingdan', + text: '订单', + iconPath: '/images/sjdingdan_icon.png', // 请替换为实际图标 + selectedIconPath: '/images/sjdingdan_icon.png' + }, + { + pagePath: 'pages/xiaoxi/xiaoxi', + text: '消息', + iconPath: '/images/xiaoxi.png', + selectedIconPath: '/images/xiaoxi.png' + }, + { + pagePath: 'pages/shangjiaduan/shangjiaduan', + text: '我的', + iconPath: '/images/shangjiaduan_icon.png', + selectedIconPath: '/images/shangjiaduan_icon.png' + } + ] + }, + // 管事 + guanshi: { + name: '管事', + defaultPage: 'pages/guanshiduan/guanshiduan', + tabList: [ + { + pagePath: 'pages/xiaoxi/xiaoxi', + text: '消息', + iconPath: '/images/xiaoxi.png', + selectedIconPath: '/images/xiaoxi.png' + }, + { + pagePath: 'pages/guanshiduan/guanshiduan', + text: '我的', + iconPath: '/images/guanshiduan_icon.png', + selectedIconPath: '/images/guanshiduan_icon.png' + } + ] + }, + // 组长 + zuzhang: { + name: '组长', + defaultPage: 'pages/zuzhangduan/zuzhangduan', + tabList: [ + { + pagePath: 'pages/xiaoxi/xiaoxi', + text: '消息', + iconPath: '/images/xiaoxi.png', + selectedIconPath: '/images/xiaoxi.png' + }, + { + pagePath: 'pages/zuzhangduan/zuzhangduan', + text: '我的', + iconPath: '/images/zuzhangduan_icon.png', + selectedIconPath: '/images/zuzhangduan_icon.png' + } + ] + } + }; + + module.exports = { roles }; \ No newline at end of file diff --git a/custom-tab-bar/index.js b/custom-tab-bar/index.js new file mode 100644 index 0000000..53ab079 --- /dev/null +++ b/custom-tab-bar/index.js @@ -0,0 +1,194 @@ +// custom-tab-bar/index.js +const app = getApp(); + +const roleCfg = { + normal: { + name: '点单老板', + defaultPage: '/pages/index/index', + tabList: [ + { pagePath: 'pages/index/index', text: '商城', iconPath: '/images/zhuye.png', selectedIconPath: '/images/zhuye.png' }, + { pagePath: 'pages/fenlei/fenlei', text: '分类', iconPath: '/images/fenlei.png', selectedIconPath: '/images/fenlei.png' }, + { pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' }, + { pagePath: 'pages/wode/wode', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' } + ] + }, + dashou: { + name: '打手', + defaultPage: '/pages/jiedan/jiedan', + tabList: [ + { pagePath: 'pages/jiedan/jiedan', text: '接单池', iconPath: '/images/jiedanchi.png', selectedIconPath: '/images/jiedanchi.png' }, + { pagePath: 'pages/dashoudingdan/dashoudingdan', text: '订单', iconPath: '/images/dingdan.png', selectedIconPath: '/images/dingdan.png' }, + { pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' }, + { pagePath: 'pages/dashouduan/dashouduan', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' } + ] + }, + shangjia: { + name: '商家', + defaultPage: 'pages/sjdingdan/sjdingdan', + tabList: [ + { pagePath: 'pages/sjdingdan/sjdingdan', text: '订单', iconPath: '/images/dingdan.png', selectedIconPath: '/images/dingdan.png' }, + { pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' }, + { pagePath: 'pages/shangjiaduan/shangjiaduan', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' } + ] + }, + guanshi: { + name: '管事', + defaultPage: '/pages/guanshiduan/guanshiduan', + tabList: [ + { pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' }, + { pagePath: 'pages/guanshiduan/guanshiduan', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' } + ] + }, + zuzhang: { + name: '组长', + defaultPage: '/pages/zuzhangduan/zuzhangduan', + tabList: [ + { pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' }, + { pagePath: 'pages/zuzhangduan/zuzhangduan', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' } + ] + }, + kaoheguan: { + name: '考核官', + defaultPage: '/pages/kaoheguan/kaoheguan', + tabList: [ + { pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' }, + { pagePath: 'pages/kaoheguan/kaoheguan', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' } + ] + } +}; + +Component({ + data: { + selectedIndex: 0, + tabList: [], + currentRole: 'normal', + showRolePicker: false, + availableRoles: [], + badgeText: '' + }, + lifetimes: { + attached() { + this.refresh(); + this._badgeListener = (data) => { + if (data.badgeText !== undefined) { + this.setData({ badgeText: data.badgeText }); + } + }; + app.on('tabBarBadgeChanged', this._badgeListener); + }, + detached() { + if (this._badgeListener) { + app.off('tabBarBadgeChanged', this._badgeListener); + } + } + }, + pageLifetimes: { + show() { + this.refresh(); + const globalBadge = app.globalData.messageManager?.tabBarBadgeText || ''; + if (this.data.badgeText !== globalBadge) { + this.setData({ badgeText: globalBadge }); + } + this._syncUnreadWithDebounce(); + } + }, + methods: { + _syncUnreadWithDebounce() { + if (this._syncTimer) clearTimeout(this._syncTimer); + this._syncTimer = setTimeout(() => { + if (app && app.loadConversations) { + app.loadConversations(); + } + }, 300); + }, + + refresh() { + const role = app.globalData.currentRole || 'normal'; + let cfg = roleCfg[role]; + if (!cfg) return; + + // 打手身份下,如果 isJinpai=1,悄悄把“我的”页面换成金牌页面 + if (role === 'dashou') { + const isJinpai = wx.getStorageSync('isJinpai') === 1; + if (isJinpai) { + cfg = { + ...cfg, + tabList: cfg.tabList.map(item => { + if (item.pagePath === 'pages/dashouduan/dashouduan') { + return { ...item, pagePath: 'pages/jinpaids/jinpaids' }; + } + return { ...item }; + }) + }; + } + } + + const list = cfg.tabList.map((v, i) => ({ ...v, index: i })); + const pages = getCurrentPages(); + const curRoute = pages.length ? pages[pages.length - 1].route : ''; + let idx = list.findIndex(t => t.pagePath === curRoute); + if (idx === -1) idx = 0; + const avail = this._getAvail(); + this.setData({ currentRole: role, tabList: list, selectedIndex: idx, availableRoles: avail }); + }, + + _getAvail() { + const arr = [{ key: 'normal', name: '点单老板' }]; + if (wx.getStorageSync('dashoustatus') === 1) arr.push({ key: 'dashou', name: '打手' }); + if (wx.getStorageSync('shangjiastatus') === 1) arr.push({ key: 'shangjia', name: '商家' }); + if (wx.getStorageSync('guanshistatus') === 1) arr.push({ key: 'guanshi', name: '管事' }); + if (wx.getStorageSync('zuzhangstatus') === 1) arr.push({ key: 'zuzhang', name: '组长' }); + if (wx.getStorageSync('kaoheguanstatus') === 1) arr.push({ key: 'kaoheguan', name: '考核官' }); + return arr; + }, + + toggleRolePicker() { + this.setData({ showRolePicker: !this.data.showRolePicker }); + }, + + async selectRole(e) { + const newRole = e.currentTarget.dataset.role; + if (newRole === this.data.currentRole) { + this.setData({ showRolePicker: false }); + return; + } + + this.setData({ showRolePicker: false }); + + // 1. 立即更新角色并保存 + app.globalData.currentRole = newRole; + wx.setStorageSync('currentRole', newRole); + + // 2. 🔥 确定跳转页面:所有角色都去“我的”页面 + let targetPage = ''; + if (newRole === 'dashou') { + // 打手需要根据金牌状态决定跳转 + const isJinpai = wx.getStorageSync('isJinpai') === 1; + targetPage = isJinpai ? '/pages/jinpaids/jinpaids' : '/pages/dashouduan/dashouduan'; + } else { + // 其他角色直接取 tabList 最后一个页面 + const cfg = roleCfg[newRole]; + if (cfg && cfg.tabList.length > 0) { + targetPage = '/' + cfg.tabList[cfg.tabList.length - 1].pagePath; + } + } + + // 3. 跳转 + if (targetPage) { + wx.reLaunch({ url: targetPage }); + } + + // 4. 后台异步切换连接 + app.switchRoleAndReconnect(newRole).catch(err => { + console.error('后台切换连接失败:', err); + }); + }, + + switchTab(e) { + const path = e.currentTarget.dataset.path; + const idx = e.currentTarget.dataset.index; + if (idx === this.data.selectedIndex) return; + wx.redirectTo({ url: '/' + path }); + } + } +}); \ No newline at end of file diff --git a/custom-tab-bar/index.json b/custom-tab-bar/index.json new file mode 100644 index 0000000..62389a9 --- /dev/null +++ b/custom-tab-bar/index.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} + } \ No newline at end of file diff --git a/custom-tab-bar/index.wxml b/custom-tab-bar/index.wxml new file mode 100644 index 0000000..93b07a6 --- /dev/null +++ b/custom-tab-bar/index.wxml @@ -0,0 +1,42 @@ + + + + + + 切换 + + + + + + + + + {{badgeText}} + + + {{item.text}} + + + + + + + + + + 切换身份 + + + + + {{item.name}} + + + + + \ No newline at end of file diff --git a/custom-tab-bar/index.wxss b/custom-tab-bar/index.wxss new file mode 100644 index 0000000..7a957fa --- /dev/null +++ b/custom-tab-bar/index.wxss @@ -0,0 +1,165 @@ +/* custom-tab-bar/index.wxss - 角标浮于右上角,可超出胶囊 */ + +.custom-tab-bar { + position: fixed; + bottom: 0; left: 0; right: 0; + z-index: 9999; + display: flex; + justify-content: center; + align-items: flex-end; + padding-bottom: env(safe-area-inset-bottom); + background: transparent; + } + + .bar-row { + display: flex; + align-items: center; + margin-bottom: 15rpx; + padding: 0 20rpx; + width: 100%; + box-sizing: border-box; + } + + /* 左侧椭圆身份按钮 */ + .role-switch-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 110rpx; + height: 100rpx; + border-radius: 50rpx; + background: #000000; + border: 1px solid rgba(255,255,255,0.15); + box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.8); + margin-right: 20rpx; + flex-shrink: 0; + } + .role-switch-icon { + width: 40rpx; + height: 40rpx; + margin-bottom: 2rpx; + } + + .role-switch-text { + font-size: 22rpx; + font-weight: 600; + color: #00f7ff; + letter-spacing: 1rpx; + } + + /* 纯黑闪光胶囊,移除overflow保证角标可见 */ + .tab-capsule { + flex: 1; + display: flex; + align-items: center; + justify-content: space-around; + height: 100rpx; + background: #000000; + border-radius: 50rpx; + border: 1px solid rgba(255,255,255,0.15); + box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.8); + padding: 0 20rpx; + position: relative; + } + .tab-capsule::before { + content: ''; + position: absolute; + top: 0; left: -100%; + width: 60%; height: 100%; + background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.25) 20%, rgba(255,255,255,0.35) 50%, rgba(255,255,255,0.25) 80%, transparent 100%); + animation: shine 3s infinite; + z-index: 1; + } + @keyframes shine { + 0% { left: -100%; } + 25% { left: 150%; } + 100% { left: 150%; } + } + + .tab-item { + position: relative; /* 角标的定位参考 */ + z-index: 2; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: 70rpx; + } + .tab-icon { + width: 44rpx; + height: 44rpx; + margin-bottom: 4rpx; + filter: brightness(0) invert(1); + opacity: 0.9; + } + .tab-text { + font-size: 20rpx; + color: #aaa; + z-index: 2; + } + .active .tab-icon { + opacity: 1; + filter: brightness(0) invert(1) drop-shadow(0 0 8rpx #00aaff); + } + .active .tab-text { + color: #00aaff; + font-weight: 500; + } + + /* 🔴 角标容器:绝对定位到图标右上角,允许超出 */ + .badge-container { + position: absolute; + top: -10rpx; /* 往上提,更接近图标右上角 */ + right: -15rpx; /* 往右伸,超出tab-item */ + z-index: 100; + pointer-events: none; /* 不阻挡点击 */ + } + .tab-badge { + min-width: 36rpx; + height: 36rpx; + line-height: 36rpx; + padding: 0 10rpx; + background: #ff3b30; + color: #fff; + font-size: 22rpx; + font-weight: bold; + border-radius: 36rpx; + text-align: center; + box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.3); + border: 1rpx solid rgba(255,255,255,0.4); + } + + /* 弹窗部分保持不变 */ + .picker-mask { + position: fixed; top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.6); + z-index: 10000; + } + .picker-panel { + position: fixed; bottom: 0; left: 0; right: 0; + background: #000000; + border-radius: 30rpx 30rpx 0 0; + z-index: 10001; + max-height: 70vh; + overflow-y: auto; + } + .picker-header { + display: flex; justify-content: space-between; align-items: center; + padding: 30rpx 40rpx 20rpx; + border-bottom: 1px solid rgba(255,255,255,0.1); + } + .picker-title { font-size: 32rpx; color: #00f7ff; } + .picker-close { font-size: 36rpx; color: #aaa; padding: 0 10rpx; } + .role-option { + display: flex; justify-content: space-between; align-items: center; + padding: 28rpx 40rpx; + border-bottom: 1px solid rgba(255,255,255,0.05); + color: #ccc; font-size: 28rpx; + } + .role-option.active { color: #00f7ff; } + .check-mark { font-size: 32rpx; color: #00f7ff; } + + page { + padding-bottom: 100rpx; + } \ No newline at end of file diff --git a/dengji/__init__.py b/dengji/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dengji/__pycache__/__init__.cpython-313.pyc b/dengji/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..e680c88 Binary files /dev/null and b/dengji/__pycache__/__init__.cpython-313.pyc differ diff --git a/dengji/__pycache__/apps.cpython-313.pyc b/dengji/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000..0ece0ff Binary files /dev/null and b/dengji/__pycache__/apps.cpython-313.pyc differ diff --git a/dengji/admin.py b/dengji/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/dengji/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/dengji/apps.py b/dengji/apps.py new file mode 100644 index 0000000..71c37fe --- /dev/null +++ b/dengji/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DengjiConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "dengji" diff --git a/dengji/migrations/__init__.py b/dengji/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dengji/models.py b/dengji/models.py new file mode 100644 index 0000000..a3e6f3d --- /dev/null +++ b/dengji/models.py @@ -0,0 +1,235 @@ +from django.db import models +from django.conf import settings + +from dingdan.models import Dingdan + + +# ==================== 板块表 ==================== +class Bankuai(models.Model): + bankuai_id = models.AutoField(primary_key=True, verbose_name='板块ID') + mingcheng = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='板块名称') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'bankuai' + verbose_name = '板块' + verbose_name_plural = verbose_name + + def __str__(self): + return self.mingcheng + + +# ==================== 打手订单表现记录 ==================== +class DashouBiaoxian(models.Model): + dingdan = models.ForeignKey('dingdan.Dingdan', to_field='dingdan_id', on_delete=models.CASCADE, related_name='biaoxian', verbose_name='关联订单', db_index=True) + + dashou_id = models.CharField(max_length=32, db_index=True, verbose_name='打手ID') + bankuai = models.ForeignKey(Bankuai, on_delete=models.PROTECT, to_field='bankuai_id', verbose_name='所属板块', db_index=True,null=True) + shangxian_sudu = models.PositiveSmallIntegerField(null=True, blank=True, verbose_name='上线速度评价(1-5)') + fuwu_taidu = models.PositiveSmallIntegerField(null=True, blank=True, verbose_name='服务态度评价(1-5)') + fanche_cishu = models.PositiveIntegerField(null=True, blank=True, verbose_name='翻车次数') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + jiedan_time = models.DateTimeField(null=True, blank=True, verbose_name='接单时间') + tijiao_time = models.DateTimeField(null=True, blank=True, verbose_name='提交时间') + + class Meta: + db_table = 'dashou_biaoxian' + verbose_name = '打手订单表现' + verbose_name_plural = verbose_name + indexes = [models.Index(fields=['dashou_id', 'bankuai', 'create_time'])] + + +# ==================== 经验变动记录 ==================== +class JingyanJilu(models.Model): + yonghu_id = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') + yonghu_leixing = models.CharField(max_length=10, choices=[('dashou','打手'),('boss','老板'),('shangjia','商家'),('guanshi','管事'),('zuzhang','组长')], verbose_name='用户类型') + bankuai = models.ForeignKey(Bankuai, on_delete=models.PROTECT, to_field='bankuai_id', verbose_name='所属板块', db_index=True) + sudu_jingyan = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name='上线速度经验') + shili_jingyan = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name='打手实力经验') + taidu_jingyan = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name='服务态度经验') + zonghe_jingyan = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name='综合经验') + dingdan_id = models.CharField(max_length=32, null=True, blank=True, db_index=True, verbose_name='关联订单ID') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + + class Meta: + db_table = 'jingyan_jilu' + verbose_name = '经验变动记录' + verbose_name_plural = verbose_name + indexes = [models.Index(fields=['yonghu_id', 'bankuai', 'create_time'])] + + +# ==================== 称号主表 ==================== +class Chenghao(models.Model): + mingcheng = models.CharField(max_length=50, unique=True, verbose_name='称号名称') + leixing = models.CharField(max_length=10, choices=[('dashou','打手'),('boss','老板'),('shangjia','商家'),('guanshi','管事'),('zuzhang','组长')], verbose_name='称号类型') + bankuai = models.ForeignKey(Bankuai, on_delete=models.PROTECT, to_field='bankuai_id', null=True, blank=True, verbose_name='所属板块(空=全板块通用)') + tubiao_url = models.CharField(max_length=200, null=True, blank=True, verbose_name='称号图标相对URL') + texiao_miaoshu = models.TextField(null=True, blank=True, verbose_name='特效描述(JSON)') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + kaohe_guize = models.TextField(null=True, blank=True, verbose_name='考核规则') # 新增 + kaioi_jinpai = models.BooleanField(default=False, verbose_name='是否同时开启金牌') + + class Meta: + db_table = 'chenghao' + verbose_name = '称号' + verbose_name_plural = verbose_name + indexes = [models.Index(fields=['leixing', 'bankuai'])] + + +# ==================== 用户 - 称号关联 ==================== +class YonghuChenghao(models.Model): + yonghu = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chenghao_list', verbose_name='用户', db_index=True) + chenghao = models.ForeignKey(Chenghao, on_delete=models.CASCADE, verbose_name='称号') + huode_time = models.DateTimeField(auto_now_add=True, verbose_name='获得时间') + + class Meta: + db_table = 'yonghu_chenghao' + verbose_name = '用户称号关联' + verbose_name_plural = verbose_name + unique_together = ('yonghu', 'chenghao') + # 修复:只索引本表字段 + indexes = [models.Index(fields=['yonghu', 'chenghao'])] + + + + + +class DingdanBiaoqian(models.Model): + """订单需求标签关联表""" + dingdan = models.ForeignKey( + Dingdan, + to_field='dingdan_id', # 关联到字符串字段 dingdan_id + on_delete=models.CASCADE, + verbose_name='订单', + db_index=True + ) + chenghao = models.ForeignKey('dengji.Chenghao', on_delete=models.CASCADE, verbose_name='标签') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'dingdan_biaoqian' + verbose_name = '订单需求标签' + verbose_name_plural = verbose_name + unique_together = ('dingdan', 'chenghao') + indexes = [models.Index(fields=['dingdan'])] + + + + + +class ShenheJilu(models.Model): + """审核记录表""" + shenqingren_id = models.CharField(max_length=7, verbose_name='申请人用户ID') + shenheguan_id = models.CharField(max_length=7, null = True,verbose_name='审核官用户ID') + zhuangtai = models.IntegerField(choices=[(0, '审核中'), (1, '通过'), (2, '未通过'), (3, '待审核')], default=0, verbose_name='审核状态') + bankuai = models.ForeignKey(Bankuai, on_delete=models.PROTECT, to_field='bankuai_id', null=True, blank=True, verbose_name='审核板块') + chenghao = models.ForeignKey(Chenghao, on_delete=models.PROTECT, null=True, blank=True, verbose_name='审核标签') + cishu = models.IntegerField(default=1, verbose_name='第几次审核') + jiaofei_jine = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='缴纳费用') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + jilu_id = models.CharField(max_length=32, unique=True, db_index=True, verbose_name='记录ID') # 新增 + guize_neirong = models.TextField(null=True, blank=True, verbose_name='规则内容') + shenheguan_youxi_id = models.CharField(max_length=50, null=True, blank=True, verbose_name='审核官游戏ID') + dashou_youxi_id = models.CharField(max_length=50, null=True, blank=True, verbose_name='打手游戏ID') + dashou_beizhu = models.TextField(null=True, blank=True, verbose_name='打手备注') + + class Meta: + db_table = 'shenhe_jilu' + verbose_name = '审核记录' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['shenqingren_id', 'zhuangtai']), + models.Index(fields=['shenheguan_id']), + ] + +class KaoheJiluFeiyong(models.Model): + """每次考核记录实际缴纳的费用明细""" + jilu = models.ForeignKey(ShenheJilu, on_delete=models.CASCADE, to_field='jilu_id', verbose_name='考核记录') + cishu = models.IntegerField(verbose_name='第几次考核') + jine = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='缴纳金额') + + class Meta: + db_table = 'kaohe_jilu_feiyong' + verbose_name = '考核记录费用' + verbose_name_plural = verbose_name + unique_together = ('jilu', 'cishu') + + + +class KaohePayTemp(models.Model): + """考核支付临时上下文(仅用于支付成功后回调取参,不删除数据)""" + dingdan_id = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='订单ID') + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') + tag_id = models.IntegerField(verbose_name='标签ID') + game_nick = models.CharField(max_length=100, verbose_name='游戏昵称') + remark = models.TextField(blank=True, verbose_name='备注') + reapply = models.BooleanField(default=False, verbose_name='是否再次申请') + jilu_id = models.CharField(max_length=32, null=True, blank=True, verbose_name='原记录ID(再次申请时)') + shenheguan_id = models.CharField(max_length=7, blank=True, verbose_name='指定考核官ID') + cishu = models.IntegerField(verbose_name='本次考核次数(预计算)') + fee = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='本次费用(分账单位)') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'kaohe_pay_temp' + verbose_name = '考核支付临时表' + verbose_name_plural = verbose_name + +class KaoheCishuFeiyong(models.Model): + """各标签各考核次数所需缴纳金额""" + chenghao = models.ForeignKey(Chenghao, on_delete=models.CASCADE, verbose_name='标签') + cishu = models.IntegerField(verbose_name='第几次考核') + feiyong = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='需缴纳金额') + + class Meta: + db_table = 'kaohe_cishu_feiyong' + verbose_name = '考核次数费用' + verbose_name_plural = verbose_name + unique_together = ('chenghao', 'cishu') + indexes = [models.Index(fields=['chenghao', 'cishu'])] + + +class KaoheJujueJilu(models.Model): + """考核拒绝原因记录""" + jilu = models.ForeignKey(ShenheJilu, on_delete=models.CASCADE, to_field='jilu_id', verbose_name='审核记录') + cishu = models.IntegerField(verbose_name='对应次数') + shenheguan_id = models.CharField(max_length=7, verbose_name='考核官用户ID') + yuanyin = models.TextField(verbose_name='拒绝原因') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'kaohe_jujue_jilu' + verbose_name = '拒绝原因记录' + verbose_name_plural = verbose_name + indexes = [models.Index(fields=['jilu_id', 'cishu'])] + + + +class KaoheguanBankuai(models.Model): + """考核官与板块多对多关联表""" + kaoheguan_id = models.CharField(max_length=7, db_index=True, verbose_name='考核官用户ID') + bankuai = models.ForeignKey(Bankuai, on_delete=models.CASCADE, to_field='bankuai_id', verbose_name='所属板块') + + class Meta: + db_table = 'kaoheguan_bankuai' + verbose_name = '考核官板块关联' + verbose_name_plural = verbose_name + unique_together = ('kaoheguan_id', 'bankuai') + indexes = [models.Index(fields=['kaoheguan_id'])] + + + + + +class ShenheGuize(models.Model): + """审核规则表""" + guize_id = models.AutoField(primary_key=True, verbose_name='规则ID') + neirong = models.TextField(verbose_name='规则内容') + + class Meta: + db_table = 'shenhe_guize' + verbose_name = '审核规则' + verbose_name_plural = verbose_name \ No newline at end of file diff --git a/dengji/services.py b/dengji/services.py new file mode 100644 index 0000000..30ffcca --- /dev/null +++ b/dengji/services.py @@ -0,0 +1,87 @@ +# dengji/services.py +import logging +from django.db import transaction, IntegrityError +from django.utils import timezone +from .models import DashouBiaoxian + +logger = logging.getLogger('dengji') + +def record_dashou_biaoxian(dingdan_id, dashou_id, xingwei, **kwargs): + """ + 统一记录打手订单表现 + :param dingdan_id: 订单ID + :param dashou_id: 打手ID + :param xingwei: 行为类型 1-抢单 2-提交订单 3-结算 + :param kwargs: 可选字段(与模型字段同名): + jiedan_time, tijiao_time, + bankuai, shangxian_sudu, fuwu_taidu, fanche_cishu + 注意:不管是哪种行为,dingdan_id 和 dashou_id 都是必传的, + 如果没传时间且业务需要时间,则自动取当前时间。 + """ + try: + # ----- 抢单:创建记录 ----- + if xingwei == 1: + jiedan_time = kwargs.get('jiedan_time') or timezone.now() + bankuai = kwargs.get('bankuai') # 可为 None + + # 避免重复创建(同一订单只会有一条表现记录) + obj, created = DashouBiaoxian.objects.get_or_create( + dingdan_id=dingdan_id, + dashou_id=dashou_id, + defaults={ + 'bankuai': bankuai, + 'jiedan_time': jiedan_time, + } + ) + if not created: + logger.info(f"表现记录已存在,跳过创建: dingdan={dingdan_id}, dashou={dashou_id}") + else: + logger.info(f"创建抢单表现记录: dingdan={dingdan_id}, dashou={dashou_id}") + + # ----- 提交订单:更新提交时间 ----- + elif xingwei == 2: + tijiao_time = kwargs.get('tijiao_time') or timezone.now() + # 只更新已存在的记录,找不到就直接跳过 + updated = DashouBiaoxian.objects.filter( + dingdan_id=dingdan_id, + dashou_id=dashou_id + ).update(tijiao_time=tijiao_time) + if updated: + logger.info(f"更新提交时间: dingdan={dingdan_id}, dashou={dashou_id}") + else: + logger.info(f"无对应记录,跳过提交时间更新: dingdan={dingdan_id}, dashou={dashou_id}") + + # ----- 结算:更新评价信息 ----- + elif xingwei == 3: + # 构建要更新的字段,只更新 kwargs 中传入的非 None 字段 + update_fields = {} + for field in ['shangxian_sudu', 'fuwu_taidu', 'fanche_cishu']: + val = kwargs.get(field) + if val is not None: + update_fields[field] = val + # 如果还传了板块也可以更新 + bankuai = kwargs.get('bankuai') + if bankuai is not None: + update_fields['bankuai'] = bankuai + + if update_fields: + updated = DashouBiaoxian.objects.filter( + dingdan_id=dingdan_id, + dashou_id=dashou_id + ).update(**update_fields) + if updated: + logger.info(f"更新评价信息: dingdan={dingdan_id}, dashou={dashou_id}, fields={update_fields}") + else: + logger.info(f"无对应记录,跳过评价更新: dingdan={dingdan_id}, dashou={dashou_id}") + else: + logger.info(f"结算时未传入任何评价字段,跳过更新") + + else: + logger.warning(f"未知的行为类型: {xingwei}") + + except IntegrityError as e: + # 唯一约束冲突(get_or_create 理论上已处理,但并发时仍可能撞上) + logger.warning(f"表现记录写入冲突,可能并发创建: {e}") + except Exception as e: + # 无论发生什么错误,都不影响主流程 + logger.error(f"记录打手表现异常: dingdan={dingdan_id}, dashou={dashou_id}, xingwei={xingwei}, error={e}", exc_info=True) \ No newline at end of file diff --git a/dengji/tests.py b/dengji/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/dengji/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/dengji/urls.py b/dengji/urls.py new file mode 100644 index 0000000..dfe1a45 --- /dev/null +++ b/dengji/urls.py @@ -0,0 +1,22 @@ +from django.urls import path +from .views import DashouBiaoqianView, BankuaiBiaoqianView, KaoheguanRegisterView, \ + KaoheguanRefreshView, ShenheDatingModuleView, ShenheDatingDataView, ShenheQiangdanView, ShenheCaozuoView, \ + ShenheXiangqingView, KaoheJinpaiTagsView, KaoheJiluView, KaoheFreeApplyView, KaoheJiluListView + +urlpatterns = [ + path('dsbqhq', DashouBiaoqianView.as_view(), name='打手标签获取'), + # 其他已有路由保持不变 + path('bkqhbq', BankuaiBiaoqianView.as_view(), name='抢单页面获取标签'), + path('khgzc', KaoheguanRegisterView.as_view(), name='审核官注册'), + path('khgsx', KaoheguanRefreshView.as_view(), name='审核官刷新'), + path('shdthqmk', ShenheDatingModuleView.as_view(), name='审核官获取板块'), + path('hqshsj', ShenheDatingDataView.as_view(), name='审核官获取待审核列表'), + path('shqd', ShenheQiangdanView.as_view(), name='审核官接待审核'), + path('hqkhxq', ShenheXiangqingView.as_view(), name='获取审核内容'), + path('khgxgkh', ShenheCaozuoView.as_view(), name='修改审核状态'), + + path('dskhhq', KaoheJinpaiTagsView.as_view(), name='考核金牌页面标签'), + path('dskhjl', KaoheJiluView.as_view(), name='打手考核记录'), + path('dssqkh', KaoheFreeApplyView.as_view(), name='免费考核申请接口'), + path('khghqkhjl', KaoheJiluListView.as_view(), name='考核官获取考核记录'), +] \ No newline at end of file diff --git a/dengji/utils.py b/dengji/utils.py new file mode 100644 index 0000000..51a85fa --- /dev/null +++ b/dengji/utils.py @@ -0,0 +1,403 @@ +# dingdan/utils.py +from .models import DingdanBiaoqian +from yonghu.models import UserMain +from dengji.models import YonghuChenghao + +def check_dashou_biaoqian_required(order_id, dashou_yonghuid): + """ + 检查打手是否满足订单的标签需求。 + - 如果订单没有需求标签,直接放行。 + - 如果有,则要求打手至少拥有其中一个标签。 + 返回 (allowed: bool, message: str) + """ + try: + # 1. 获取订单的需求标签ID集合 + required_ids = set( + DingdanBiaoqian.objects.filter(dingdan_id=order_id) + .values_list('chenghao_id', flat=True) + ) + if not required_ids: + return True, '' + + # 2. 找到打手对应的用户记录 + try: + user = UserMain.objects.get(yonghuid=dashou_yonghuid) + except UserMain.DoesNotExist: + return False, '用户信息异常' + + # 3. 获取打手已拥有的标签ID集合(只取打手类型) + owned_ids = set( + YonghuChenghao.objects.filter( + yonghu=user, + chenghao__leixing='dashou' + ).values_list('chenghao_id', flat=True) + ) + + if not owned_ids: + return False, '您未拥有任何打手标签,无法抢此订单' + + # 4. 交集判断 + if required_ids.intersection(owned_ids): + return True, '' + else: + return False, '您的标签不符合此订单要求,无法抢单' + + except Exception as e: + # 标签校验失败不影响主流程,记录日志并保守放行(也可保守拒绝,按需求选择) + import logging + logger = logging.getLogger('dingdan') + logger.error(f"标签校验异常: {e}", exc_info=True) + # 安全起见,如果校验出错,选择拒绝,避免越权 + return False, '订单标签校验异常,请稍后重试' + + + + + + + +# dengji/utils.py +from .models import Chenghao, KaoheCishuFeiyong, ShenheJilu, KaoheJiluFeiyong, YonghuChenghao, KaoheguanBankuai +from yonghu.models import UserMain, UserShenheguan, UserDashou +from django.db import models +from decimal import Decimal +from django.db.models import Q, Max +import time +import random +import logging +from django.db.models import F, Max +logger = logging.getLogger('dengji') + + + +def get_tag_fee(chenghao, cishu): + """ + 获取标签在指定考核次数下的费用。 + - 若有对应次数的配置,直接返回。 + - 若没有该次数,取该标签下已有最大次数的费用(作为后续次数的价格)。 + - 若该标签完全没有费用记录,返回 0.00(免费)。 + """ + fee_obj = KaoheCishuFeiyong.objects.filter(chenghao=chenghao, cishu=cishu).first() + if fee_obj: + return fee_obj.feiyong + + max_obj = KaoheCishuFeiyong.objects.filter(chenghao=chenghao).order_by('-cishu').first() + if max_obj: + return max_obj.feiyong + + return Decimal('0.00') + + +def generate_unique_jilu_id(): + """ + 生成唯一的审核记录 ID,格式:SH + 时间戳(10位) + 随机数(4位) + 循环检查数据库避免重复,最多尝试 10 次。 + """ + for _ in range(10): + timestamp = str(int(time.time() * 1000))[-10:] + random_part = str(random.randint(1000, 9999)) + jilu_id = f'SH{timestamp}{random_part}' + if not ShenheJilu.objects.filter(jilu_id=jilu_id).exists(): + return jilu_id + raise Exception('无法生成唯一记录ID,请稍后重试') + + +def validate_shenheguan(shenheguan_id, bankuai, fee, applicant_user): + """ + 校验指定考核官是否合法 + - 考核官必须存在且未被禁用 + - 必须属于对应板块 + - 必须处于空闲状态 + - 若费用>0,则要求已认证 + 返回 (valid, error_msg) + """ + if not shenheguan_id: + return True, '' + try: + kg = UserShenheguan.objects.select_related('user').get( + user__yonghuid=shenheguan_id, + zhuangtai=1 + ) + except UserShenheguan.DoesNotExist: + return False, '考核官不存在或已被禁用' + + # 板块权限 + if not KaoheguanBankuai.objects.filter(kaoheguan_id=shenheguan_id, bankuai=bankuai).exists(): + return False, '该考核官不属于此板块' + + # 空闲状态 + if kg.shenhe_zhuangtai != 0: + return False, '该考核官当前忙碌,无法指定' + + # 费用>0需要认证 + if float(fee) > 0 and not kg.is_renzheng: + return False, '费用大于0时只能指定已认证的考核官' + + return True, '' + + +def create_shenhe_jilu(user, tag_id, game_nick, remark, reapply, jilu_id=None, shenheguan_id=''): + """ + 创建或更新审核记录。 + - 首次申请:创建新记录,生成唯一 jilu_id。 + - 再次申请:基于原记录(必须状态为未通过)更新次数、累加总费用、更新备注和游戏昵称, + 保留审核官信息,状态重置为待审核。不创建新记录。 + - 每次申请均创建一条费用明细记录(KaoheJiluFeiyong),记录本次缴费。 + - 若原记录有考核官,则将其状态改为审核中。 + - 新增 shenheguan_id 参数:指定考核官ID,仅在原记录无考核官时可传入。 + 返回 (success, message, record) + """ + try: + # 1. 校验标签存在 + try: + chenghao = Chenghao.objects.get(id=tag_id) + except Chenghao.DoesNotExist: + return False, '标签不存在', None + + # 2. 判断用户是否已拥有该标签 + if YonghuChenghao.objects.filter(yonghu=user, chenghao=chenghao).exists(): + return False, '您已拥有该标签,无需重复考核', None + + # 3. 检查是否有进行中的审核记录(待审核、审核中) + has_pending = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + chenghao=chenghao, + zhuangtai__in=[0, 3] + ).exists() + if has_pending: + return False, '您有未完成的考核申请,请等待结果', None + + last_record = None + current_fee = Decimal('0.00') + + # 4. 处理再次申请 + if reapply and jilu_id: + # 查询原记录(未通过状态) + last_record = ShenheJilu.objects.filter( + jilu_id=jilu_id, + shenqingren_id=user.yonghuid, + zhuangtai=2 + ).first() + if not last_record: + return False, '原记录不存在或状态不是未通过', None + + new_cishu = last_record.cishu + 1 + current_fee = get_tag_fee(chenghao, new_cishu) + + # 确定最终状态:有考核官 = 审核中(0),否则待审核(3) + final_kg_id = last_record.shenheguan_id # 保留原来的 + if shenheguan_id and not last_record.shenheguan_id: + final_kg_id = shenheguan_id # 新指定 + + final_zhuangtai = 0 if final_kg_id else 3 # 有考核官就是审核中 + if final_zhuangtai == 3: + final_kg_id = None + + # 原子化更新 + ShenheJilu.objects.filter(pk=last_record.pk).update( + cishu=new_cishu, + jiaofei_jine=F('jiaofei_jine') + current_fee, + dashou_youxi_id=game_nick, + dashou_beizhu=remark, + guize_neirong=chenghao.kaohe_guize or '', + zhuangtai=final_zhuangtai, + shenheguan_id=final_kg_id, + ) + last_record.refresh_from_db() + record = last_record + + # 如果原考核官被替换或清除,释放旧考核官 + if last_record.shenheguan_id != final_kg_id: + if last_record.shenheguan_id: + UserShenheguan.objects.filter( + user__yonghuid=last_record.shenheguan_id + ).update(shenhe_zhuangtai=0) + else: + # 首次申请 + max_cishu = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + chenghao=chenghao, + zhuangtai__in=[1, 2] + ).aggregate(max=Max('cishu'))['max'] or 0 + new_cishu = max_cishu + 1 + current_fee = get_tag_fee(chenghao, new_cishu) + + bankuai = chenghao.bankuai + rule_text = chenghao.kaohe_guize or '' + new_jilu_id = generate_unique_jilu_id() + + initial_zhuangtai = 0 if shenheguan_id else 3 # 指定→审核中,否则待审核 + record = ShenheJilu.objects.create( + jilu_id=new_jilu_id, + shenqingren_id=user.yonghuid, + zhuangtai=initial_zhuangtai, + bankuai=bankuai, + chenghao=chenghao, + cishu=new_cishu, + jiaofei_jine=current_fee, + dashou_youxi_id=game_nick, + dashou_beizhu=remark, + guize_neirong=rule_text, + shenheguan_id=shenheguan_id if shenheguan_id else None, + ) + + # 5. 创建费用明细(防重复) + KaoheJiluFeiyong.objects.get_or_create( + jilu=record, + cishu=record.cishu, + defaults={'jine': current_fee} + ) + + # 6. 更新考核官统计(仅当状态为审核中且有考核官) + if record.zhuangtai == 0 and record.shenheguan_id: + UserShenheguan.objects.filter( + user__yonghuid=record.shenheguan_id + ).update( + shenhe_zongshu=F('shenhe_zongshu') + 1, + shenhe_zhuangtai=1 + ) + + return True, '申请成功', record + + except Exception as e: + logger.error(f"创建审核记录异常: {e}", exc_info=True) + return False, '系统异常', None + + +def create_shenhe_jilu_from_temp(user, temp_data): + """ + 根据临时表数据创建审核记录(专供支付回调使用) + 参数 temp_data 是 KaohePayTemp 对象 + 返回 (success, message, record) + """ + from .models import ShenheJilu, KaoheJiluFeiyong, Chenghao, YonghuChenghao + from django.db.models import F + import logging + logger = logging.getLogger('dengji') + + try: + logger.info(f"开始从临时表创建审核记录: 用户={user.yonghuid}, 订单={temp_data.dingdan_id}") + + chenghao = Chenghao.objects.get(id=temp_data.tag_id) + logger.info(f"获取标签成功: {chenghao.mingcheng}") + + # 检查是否已拥有标签 + if YonghuChenghao.objects.filter(yonghu=user, chenghao=chenghao).exists(): + logger.warning(f"用户 {user.yonghuid} 已拥有标签 {chenghao.mingcheng}") + return False, '您已拥有该标签,无需重复考核', None + + # 检查是否有进行中的审核 + if ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + chenghao=chenghao, + zhuangtai__in=[0, 3] + ).exists(): + logger.warning(f"用户 {user.yonghuid} 有未完成的考核申请") + return False, '您有未完成的考核申请,请等待结果', None + + # 使用临时表中预计算好的次数和费用 + new_cishu = temp_data.cishu + current_fee = temp_data.fee + reapply = temp_data.reapply + jilu_id = temp_data.jilu_id + shenheguan_id = temp_data.shenheguan_id + + logger.info( + f"临时表数据: cishu={new_cishu}, fee={current_fee}, reapply={reapply}, jilu_id={jilu_id}, shenheguan_id={shenheguan_id}") + + # 再次申请校验 + if reapply: + if not jilu_id: + logger.error("再次申请但未提供原记录ID") + return False, '再次申请必须提供原记录ID', None + last = ShenheJilu.objects.filter( + jilu_id=jilu_id, + shenqingren_id=user.yonghuid, + zhuangtai=2 + ).first() + if not last: + logger.error(f"原记录不存在或状态不是未通过: jilu_id={jilu_id}") + return False, '原记录不存在或状态不是未通过', None + if new_cishu != last.cishu + 1: + logger.error(f"考核次数不一致: 临时表={new_cishu}, 原记录次数={last.cishu}") + return False, '考核次数不一致', None + logger.info(f"再次申请校验通过,原记录ID: {jilu_id}") + + # 创建或更新审核记录 + if reapply and jilu_id: + # 更新原记录 + final_kg_id = last.shenheguan_id + if shenheguan_id and not last.shenheguan_id: + final_kg_id = shenheguan_id + final_zhuangtai = 0 if final_kg_id else 3 + if final_zhuangtai == 3: + final_kg_id = None + + ShenheJilu.objects.filter(pk=last.pk).update( + cishu=new_cishu, + jiaofei_jine=F('jiaofei_jine') + current_fee, + dashou_youxi_id=temp_data.game_nick, + dashou_beizhu=temp_data.remark, + guize_neirong=chenghao.kaohe_guize or '', + zhuangtai=final_zhuangtai, + shenheguan_id=final_kg_id, + ) + last.refresh_from_db() + record = last + + # 释放旧考核官 + if last.shenheguan_id != final_kg_id and last.shenheguan_id: + UserShenheguan.objects.filter(user__yonghuid=last.shenheguan_id).update(shenhe_zhuangtai=0) + logger.info(f"释放旧考核官: {last.shenheguan_id}") + + logger.info(f"更新审核记录成功: jilu_id={record.jilu_id}, 新次数={new_cishu}") + else: + # 首次申请 + bankuai = chenghao.bankuai + rule_text = chenghao.kaohe_guize or '' + new_jilu_id = generate_unique_jilu_id() + initial_zhuangtai = 0 if shenheguan_id else 3 + + record = ShenheJilu.objects.create( + jilu_id=new_jilu_id, + shenqingren_id=user.yonghuid, + zhuangtai=initial_zhuangtai, + bankuai=bankuai, + chenghao=chenghao, + cishu=new_cishu, + jiaofei_jine=current_fee, + dashou_youxi_id=temp_data.game_nick, + dashou_beizhu=temp_data.remark, + guize_neirong=rule_text, + shenheguan_id=shenheguan_id if shenheguan_id else None, + ) + logger.info(f"创建审核记录成功: jilu_id={new_jilu_id}, 状态={initial_zhuangtai}") + + # 记录费用明细 + fee_detail, created = KaoheJiluFeiyong.objects.get_or_create( + jilu=record, + cishu=record.cishu, + defaults={'jine': current_fee} + ) + if created: + logger.info(f"创建费用明细成功: 记录ID={record.jilu_id}, 金额={current_fee}") + else: + logger.warning(f"费用明细已存在: 记录ID={record.jilu_id}, 次数={record.cishu}") + + # 更新考核官状态 + if record.zhuangtai == 0 and record.shenheguan_id: + UserShenheguan.objects.filter(user__yonghuid=record.shenheguan_id).update( + shenhe_zongshu=F('shenhe_zongshu') + 1, + shenhe_zhuangtai=1 + ) + logger.info(f"更新考核官状态: {record.shenheguan_id}, 审核总数+1, 状态=忙碌") + + logger.info(f"从临时表创建审核记录完成: jilu_id={record.jilu_id}") + return True, '申请成功', record + + except Chenghao.DoesNotExist: + logger.error(f"标签不存在: tag_id={temp_data.tag_id}") + return False, '标签不存在', None + except Exception as e: + logger.error(f"从临时表创建审核记录异常: {str(e)}", exc_info=True) + return False, '系统异常', None \ No newline at end of file diff --git a/dengji/views.py b/dengji/views.py new file mode 100644 index 0000000..e1631a4 --- /dev/null +++ b/dengji/views.py @@ -0,0 +1,975 @@ +# dengji/views.py +import logging +import json +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from shangpin.models import ShangpinLeixing +from .models import YonghuChenghao, Chenghao, KaoheCishuFeiyong, KaoheJujueJilu +from .utils import get_tag_fee, create_shenhe_jilu + +logger = logging.getLogger('dengji') + + +class DashouBiaoqianView(APIView): + """获取当前打手所有称号标签""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + # 查询用户的所有称号关联,一次连表取出 + relations = YonghuChenghao.objects.filter( + yonghu=yonghu + ).select_related('chenghao') + + chenghao_list = [] + for rel in relations: + ch = rel.chenghao + # 解析特效描述 JSON(前端需要对象) + texiao_json = {} + if ch.texiao_miaoshu: + try: + texiao_json = json.loads(ch.texiao_miaoshu) + except json.JSONDecodeError: + texiao_json = {} + chenghao_list.append({ + 'mingcheng': ch.mingcheng, + 'tubiao_url': ch.tubiao_url or '', + 'texiao_json': texiao_json, + }) + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': {'chenghao_list': chenghao_list} + }) + except Exception as e: + logger.error(f"获取打手标签失败: {e}", exc_info=True) + return Response({'code': 1, 'msg': '服务器错误', 'data': None}) + + + +from shangpin.models import ShangpinLeixing +from .models import YonghuChenghao, Chenghao +class BankuaiBiaoqianView(APIView): + """获取商品类型所属板块的所有标签""" + permission_classes = [IsAuthenticated] + + def post(self, request): + leixing_id = request.data.get('leixing_id') + if not leixing_id: + return Response({'code': 1, 'msg': '缺少leixing_id'}) + + try: + leixing = ShangpinLeixing.objects.select_related('bankuai').get(id=leixing_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 2, 'msg': '商品类型不存在'}) + + bankuai = leixing.bankuai + if not bankuai: + return Response({'code': 0, 'data': {'biaoqian_list': []}}) + + # 获取该板块下所有类型的标签(leixing可以为dashou/shangjia等,这里先返回所有) + chenghao_list = Chenghao.objects.filter(bankuai=bankuai) + data = [] + for ch in chenghao_list: + texiao_json = {} + if ch.texiao_miaoshu: + try: + texiao_json = json.loads(ch.texiao_miaoshu) + except: + pass + data.append({ + 'id': ch.id, + 'mingcheng': ch.mingcheng, + 'texiao_json': texiao_json + }) + return Response({'code': 0, 'data': {'biaoqian_list': data}}) + + + + + +# dengji/views.py +import logging +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from .models import KaoheguanBankuai, Bankuai +from django.db import transaction +from yonghu.models import UserShenheguan, UserMain, UserDashou + +logger = logging.getLogger('dengji') + +# 硬编码邀请码 +KAOHEGUAN_INVITE_CODE = 'KHG2026VIP' + +class KaoheguanRegisterView(APIView): + """ + 考核官注册接口 + POST /dengji/khgzc + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + invite_code = request.data.get('inviteCode', '').strip() + + if not invite_code: + return Response({'code': 400, 'msg': '邀请码不能为空'}) + if invite_code != KAOHEGUAN_INVITE_CODE: + return Response({'code': 400, 'msg': '邀请码错误'}) + + kg = UserShenheguan.objects.filter(user=yonghu).first() + if kg: + if kg.zhuangtai == 1: + return Response({'code': 200, 'msg': '您已是考核官', 'data': self._build_data(kg)}) + else: + return Response({'code': 400, 'msg': '账号已被封禁'}) + + with transaction.atomic(): + kg = UserShenheguan.objects.create( + user=yonghu, + zhuangtai=1, + shenhe_zhuangtai=0, + shenhe_zongshu=0, + tongguo_zongshu=0, + yue=0.00, + zonge=0.00 + ) + return Response({'code': 200, 'msg': '注册成功', 'data': self._build_data(kg)}) + + except Exception as e: + logger.error(f"考核官注册失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + def _build_data(self, kg): + bankuai_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=kg.user.yonghuid).select_related('bankuai') + bankuai_list = [{'bankuai_id': b.bankuai.bankuai_id, 'mingcheng': b.bankuai.mingcheng} for b in bankuai_qs] + return { + 'zhuangtai': kg.zhuangtai, + 'shenhe_zongshu': kg.shenhe_zongshu, + 'tongguo_zongshu': kg.tongguo_zongshu, + 'yue': str(kg.yue), + 'zonge': str(kg.zonge), + 'shenhe_zhuangtai': kg.shenhe_zhuangtai, + 'bankuai_list': bankuai_list, + 'kaoheguanstatus': 1 + } + + + + + +class KaoheguanRefreshView(APIView): + """ + 考核官数据刷新接口 + POST /dengji/khgsx + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + kg = UserShenheguan.objects.filter(user=yonghu).first() + if not kg: + return Response({'code': 400, 'msg': '您还未注册考核官'}) + + if kg.zhuangtai != 1: + return Response({'code': 200, 'data': {'zhuangtai': kg.zhuangtai, 'kaoheguanstatus': 0}}) + + # 查询所属板块 + bankuai_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=kg.user.yonghuid).select_related('bankuai') + bankuai_list = [{'bankuai_id': b.bankuai.bankuai_id, 'mingcheng': b.bankuai.mingcheng} for b in bankuai_qs] + + # 统计待审核 / 审核中的记录数量 + pending_count = ShenheJilu.objects.filter( + shenheguan_id=yonghu.yonghuid, + zhuangtai=0 # 审核中 + ).count() + + data = { + 'zhuangtai': kg.zhuangtai, + 'shenhe_zongshu': kg.shenhe_zongshu, + 'tongguo_zongshu': kg.tongguo_zongshu, + 'yue': str(kg.yue), + 'zonge': str(kg.zonge), + 'shenhe_zhuangtai': kg.shenhe_zhuangtai, + 'bankuai_list': bankuai_list, + 'kaoheguanstatus': 1, + 'is_renzheng': kg.is_renzheng, # 🆕 是否认证 + 'pending_shenhe_count': pending_count # 🆕 审核中数量 + } + return Response({'code': 200, 'data': data}) + + except Exception as e: + logger.error(f"考核官刷新失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + +import json +from django.db import transaction, IntegrityError +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from .models import Bankuai, ShenheJilu, KaoheguanBankuai, Chenghao + +class ShenheDatingModuleView(APIView): + """审核大厅获取模块列表(板块 + 待审核数量 + 是否关联)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + related_ids = set(KaoheguanBankuai.objects.filter(kaoheguan_id=yonghu.yonghuid) + .values_list('bankuai_id', flat=True)) + + bankuai_list = [] + for bk in Bankuai.objects.all(): + count = ShenheJilu.objects.filter(bankuai=bk, zhuangtai=3, shenheguan_id__isnull=True).count() + bankuai_list.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'count': count, + 'is_related': bk.bankuai_id in related_ids + }) + + return Response({'code': 0, 'data': {'modules': bankuai_list}}) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + + +class ShenheDatingDataView(APIView): + """获取审核大厅待审核记录列表""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + data = request.data + bankuai_id = data.get('bankuai_id') + page = int(data.get('page', 1)) + page_size = int(data.get('page_size', 5)) + + query = ShenheJilu.objects.filter(zhuangtai=3, shenheguan_id__isnull=True) + if bankuai_id: + query = query.filter(bankuai_id=bankuai_id) + + total = query.count() + offset = (page - 1) * page_size + jilu_list = query.select_related('bankuai', 'chenghao').order_by('-create_time')[ + offset:offset + page_size] + + records = [] + for jl in jilu_list: + user = UserMain.objects.filter(yonghuid=jl.shenqingren_id).first() + avatar = user.avatar if user else '' + texiao_json = {} + if jl.chenghao and jl.chenghao.texiao_miaoshu: + try: + texiao_json = json.loads(jl.chenghao.texiao_miaoshu) + except: + pass + + # 新增字段:本次考核费用 + kaohe_feiyong = float(jl.jiaofei_jine) if jl.jiaofei_jine is not None else 0.0 + + records.append({ + 'jilu_id': jl.jilu_id, + 'shenqingren_id': jl.shenqingren_id, + 'shenqingren_avatar': avatar, + 'dashou_youxi_id': jl.dashou_youxi_id or '', + 'dashou_beizhu': jl.dashou_beizhu or '', + 'bankuai_id': jl.bankuai.bankuai_id if jl.bankuai else None, + 'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '', + 'chenghao_id': jl.chenghao_id, + 'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '', + 'chenghao_texiao_json': texiao_json, + 'guize_neirong': jl.guize_neirong or '', + 'kaohe_feiyong': kaohe_feiyong, # 新增 + 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S') if jl.create_time else '', + }) + + has_more = (page * page_size) < total + return Response({'code': 0, 'data': {'list': records, 'has_more': has_more, 'total': total}}) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + +class ShenheQiangdanView(APIView): + """ + 审核官抢单(接审核任务) + 校验:身份、账号封禁、已有进行中审核、记录状态、不能审核自己、板块权限 + 成功后更新考核官扩展表:审核总数+1,审核状态设为“审核中” + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + jilu_id = request.data.get('jilu_id') + if not jilu_id: + return Response({'code': 400, 'msg': '记录ID不能为空'}) + + # 1. 检查考核官身份及账号是否被禁用 + kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() + if not kg: + return Response({'code': 403, 'msg': '您不是考核官或账号已被禁用'}) + + # 2. 检查是否有正在审核中的记录(zhuangtai=0),防止同时审核多个 + ongoing = ShenheJilu.objects.filter( + shenheguan_id=yonghu.yonghuid, zhuangtai=0 + ).exists() + if ongoing: + return Response({'code': 400, 'msg': '您有正在审核中的任务,请完成后再接新任务'}) + + with transaction.atomic(): + # 3. 行锁抢单:仅当状态为待审核(3)且无审核官时才能抢 + jilu = ShenheJilu.objects.select_for_update().filter( + jilu_id=jilu_id, + zhuangtai=3, + shenheguan_id__isnull=True + ).first() + if not jilu: + return Response({'code': 400, 'msg': '该审核任务已被接走、状态变更或不存在'}) + + # 4. 不能审核自己 + if jilu.shenqingren_id == yonghu.yonghuid: + return Response({'code': 400, 'msg': '不能审核自己提交的申请'}) + + # 5. 板块权限:考核官必须属于该板块 + is_related = KaoheguanBankuai.objects.filter( + kaoheguan_id=yonghu.yonghuid, + bankuai=jilu.bankuai + ).exists() + if not is_related: + return Response({'code': 400, 'msg': '您不属于该板块,无法审核'}) + + # 新增:6. 费用>0时,要求考核官已认证 + if jilu.jiaofei_jine and float(jilu.jiaofei_jine) > 0: + if not kg.is_renzheng: + return Response({'code': 400, 'msg': '本考核需要认证考核官接单,您尚未认证'}) + + # 7. 更新审核记录:分配给当前考核官,状态变为审核中 + jilu.zhuangtai = 0 # 审核中 + jilu.shenheguan_id = yonghu.yonghuid + jilu.save() + + # 8. 更新考核官扩展表:审核总数 +1,审核状态置为审核中(1) + UserShenheguan.objects.filter(user=yonghu).update( + shenhe_zongshu=F('shenhe_zongshu') + 1, + shenhe_zhuangtai=1 + ) + + return Response({ + 'code': 0, + 'msg': '抢单成功,请前往游戏内审核,审核完成后请到考核打分页面进行评定。', + 'data': {'bankuai_id': jilu.bankuai.bankuai_id} + }) + + except Exception as e: + logger.error(f"审核官抢单异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) + + + + + + +import json +import logging +from decimal import Decimal + +from django.db import transaction, models +from django.db.models import F, Q, OuterRef, Subquery, Count, Max +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +class ShenheXiangqingView(APIView): + """获取当前考核官审核中的记录详情(新增待收考核费字段)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() + if not kg: + return Response({'code': 403, 'msg': '您不是考核官或账号异常'}) + + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + + # 查询当前考核官名下状态为“审核中”的记录 + base_query = ShenheJilu.objects.filter( + shenheguan_id=yonghu.yonghuid, + zhuangtai=0 + ).select_related('bankuai', 'chenghao') + total = base_query.count() + offset = (page - 1) * page_size + jilu_list = base_query[offset:offset + page_size] + + records = [] + for jl in jilu_list: + user = UserMain.objects.filter(yonghuid=jl.shenqingren_id).first() + avatar = user.avatar if user else '' + texiao_json = {} + if jl.chenghao and jl.chenghao.texiao_miaoshu: + try: + texiao_json = json.loads(jl.chenghao.texiao_miaoshu) + except: + pass + + # 是否开启金牌 + is_jinpai = jl.chenghao.kaioi_jinpai if jl.chenghao else False + + # 本次待收考核费(基于记录中的当前次数,从费用明细表查询) + daishou_fei = 0.00 + fee_detail = KaoheJiluFeiyong.objects.filter( + jilu=jl, + cishu=jl.cishu + ).first() + if fee_detail: + daishou_fei = float(fee_detail.jine) + else: + # 兜底:若明细表无记录,尝试用通用费用表计算 + daishou_fei = 0.00 + + # 上次失败原因 + last_fail_reason = '' + last_fail = KaoheJujueJilu.objects.filter( + jilu=jl, + cishu=jl.cishu - 1 + ).order_by('-create_time').first() + if last_fail: + last_fail_reason = last_fail.yuanyin + + records.append({ + 'jilu_id': jl.jilu_id, + 'shenqingren_id': jl.shenqingren_id, + 'shenqingren_avatar': avatar, + 'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '', + 'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '', + 'chenghao_texiao_json': texiao_json, + 'guize_neirong': jl.guize_neirong or '', + 'dashou_beizhu': jl.dashou_beizhu or '', + 'dashou_youxi_id': jl.dashou_youxi_id or '', + 'shenheguan_youxi_id': jl.shenheguan_youxi_id or '', + 'cishu': jl.cishu, + 'daishou_fei': daishou_fei, # 新增:待收考核费 + 'last_fail_reason': last_fail_reason, + 'is_jinpai': is_jinpai, + 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S') if jl.create_time else '', + }) + + has_more = (page * page_size) < total + return Response({'code': 0, 'data': {'list': records, 'has_more': has_more, 'total': total}}) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + +class ShenheCaozuoView(APIView): + """ + 考核官执行操作(同意/拒绝/转移/修改昵称),并更新考核官收益 + - 只有“同意”或“拒绝”才增加余额和总额,金额严格从 KaoheJiluFeiyong 表获取 + - 查不到费用则加 0 元,绝不影响流程 + - 转移、修改昵称等操作不涉及收益 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + # 1. 验证考核官身份及账号状态 + kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() + if not kg: + return Response({'code': 403, 'msg': '账号异常,无法操作'}) + + jilu_id = request.data.get('jilu_id') + caozuo = request.data.get('caozuo') # agree / reject / transfer / modify_nick + if not jilu_id or not caozuo: + return Response({'code': 400, 'msg': '缺少必要参数'}) + + with transaction.atomic(): + # 2. 锁定审核记录,确保属于当前考核官且状态为审核中(0) + jilu = ShenheJilu.objects.select_for_update().filter( + jilu_id=jilu_id, + shenheguan_id=yonghu.yonghuid, + zhuangtai=0 + ).first() + if not jilu: + return Response({'code': 400, 'msg': '该记录不存在或不属于您'}) + + # 3. 本次操作产生的收入(仅通过/拒绝时有意义) + daishou_fei = 0.00 + + # 4. 根据操作类型执行对应业务逻辑 + if caozuo == 'agree': + # --- 通过考核 --- + # 查询本次考核应得的费用(基于记录ID和当前次数) + fee_detail = KaoheJiluFeiyong.objects.filter( + jilu=jilu, + cishu=jilu.cishu + ).first() + if fee_detail: + daishou_fei = float(fee_detail.jine) + # 找不到就保持 0,绝不使用任何默认值 + + jilu.zhuangtai = 1 + jilu.save() + + # 授予标签 + if jilu.chenghao: + yh = UserMain.objects.get(yonghuid=jilu.shenqingren_id) + if not YonghuChenghao.objects.filter(yonghu=yh, chenghao=jilu.chenghao).exists(): + YonghuChenghao.objects.create(yonghu=yh, chenghao=jilu.chenghao) + # 如果标签开启了金牌,同步更新打手称号 + if jilu.chenghao.kaioi_jinpai: + dashou_prof = UserDashou.objects.filter(user=yh).first() + if dashou_prof: + dashou_prof.chenghao = '金牌选手' + dashou_prof.save() + + elif caozuo == 'reject': + # --- 拒绝考核 --- + yuanyin = request.data.get('yuanyin', '') + if not yuanyin.strip(): + return Response({'code': 400, 'msg': '请填写拒绝原因'}) + + # 查询本次考核应得的费用(拒绝时同样获得收入) + fee_detail = KaoheJiluFeiyong.objects.filter( + jilu=jilu, + cishu=jilu.cishu + ).first() + if fee_detail: + daishou_fei = float(fee_detail.jine) + # 找不到就保持 0 + + jilu.zhuangtai = 2 + jilu.save() + # 记录拒绝原因 + KaoheJujueJilu.objects.create( + jilu=jilu, + cishu=jilu.cishu, + shenheguan_id=yonghu.yonghuid, + yuanyin=yuanyin + ) + + # 新增:不合格时,如果考核官未认证,则清空考核官ID和游戏昵称 + if not kg.is_renzheng: + jilu.shenheguan_id = None + jilu.shenheguan_youxi_id = '' + jilu.save(update_fields=['shenheguan_id', 'shenheguan_youxi_id']) + + elif caozuo == 'transfer': + # 转移考核官:清空考核官信息,状态回退为待审核 + jilu.shenheguan_id = None + jilu.shenheguan_youxi_id = None + jilu.zhuangtai = 3 + jilu.save() + # 转移不更新收益,直接返回 + + + elif caozuo == 'modify_nick': + # 修改考核官游戏昵称 + youxi_id = request.data.get('youxi_id', '').strip() + if not youxi_id: + return Response({'code': 400, 'msg': '请输入游戏昵称'}) + jilu.shenheguan_youxi_id = youxi_id + jilu.save() + return Response({'code': 0, 'msg': '修改成功'}) + + else: + return Response({'code': 400, 'msg': '无效的操作类型'}) + + # 5. 更新考核官收益(仅“通过”或“拒绝”时执行) + if caozuo in ('transfer'): + update_fields = { + + 'shenhe_zhuangtai': 0, # 设为空闲 + } + UserShenheguan.objects.filter(user=yonghu).update(**update_fields) + if caozuo in ('agree', 'reject'): + update_fields = { + 'yue': F('yue') + Decimal(str(daishou_fei)), + 'zonge': F('zonge') + Decimal(str(daishou_fei)), + 'shenhe_zhuangtai': 0, # 设为空闲 + } + # 只有同意时才增加“通过总数” + if caozuo == 'agree': + update_fields['tongguo_zongshu'] = F('tongguo_zongshu') + 1 + + # 审核总数不再增加 + UserShenheguan.objects.filter(user=yonghu).update(**update_fields) + + return Response({'code': 0, 'msg': '操作成功'}) + + except Exception as e: + logger.error(f"考核操作异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) + + + + + +# dengji/views.py +class KaoheJinpaiTagsView(APIView): + """获取考核金牌页面的标签和板块信息""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + # 获取用户已拥有的标签名称集合 + owned_names = set( + YonghuChenghao.objects.filter(yonghu=user, chenghao__leixing='dashou') + .values_list('chenghao__mingcheng', flat=True) + ) + + # 板块列表 + plate_list = [] + for bk in Bankuai.objects.all(): + tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou') + tag_list = [] + for tag in tags: + # 费用列表 + fee_objs = KaoheCishuFeiyong.objects.filter(chenghao=tag).order_by('cishu') + fee_list = [{'cishu': f.cishu, 'feiyong': float(f.feiyong)} for f in fee_objs] + + # 若没有记录,补充默认费用 + if not fee_list: + fee_list = [{'cishu': 1, 'feiyong': 5.00}] + + tag_list.append({ + 'id': tag.id, + 'name': tag.mingcheng, + 'texiao_json': json.loads(tag.texiao_miaoshu) if tag.texiao_miaoshu else {}, + 'jinpai': tag.kaioi_jinpai, + 'rule': tag.kaohe_guize or '', + 'fee_list': fee_list, + 'owned': tag.mingcheng in owned_names, + }) + + if tag_list: + plate_list.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'tags': tag_list + }) + + # 新增:返回所有在职考核官信息(用于指定考核官) + kaoheguan_list = [] + for kg in UserShenheguan.objects.filter(zhuangtai=1).select_related('user'): + uid = kg.user.yonghuid + avatar = kg.user.avatar or '' + # 获取考核官的打手昵称(从打手扩展表) + dashou_nick = '' + dashou_prof = UserDashou.objects.filter(user=kg.user).first() + if dashou_prof: + dashou_nick = dashou_prof.nicheng or '' + kaoheguan_list.append({ + 'yonghuid': uid, + 'avatar': avatar, + 'dashou_nick': dashou_nick, + 'is_renzheng': kg.is_renzheng, + 'shenhe_zhuangtai': kg.shenhe_zhuangtai, + }) + + return Response({ + 'code': 0, + 'data': { + 'plate_list': plate_list, + 'my_tags': list(owned_names), + 'kaoheguan_list': kaoheguan_list, # 新增 + } + }) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + +# dengji/utils.py +from .models import Chenghao, KaoheCishuFeiyong, ShenheJilu, KaoheJiluFeiyong, YonghuChenghao +from yonghu.models import UserMain +from django.db import models +class KaoheJiluView(APIView): + """获取打手本人的考核记录(支持筛选状态)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + status = request.data.get('status', 'pending') # pending / passed + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 5)) + + if status == 'pending': + # 进行中/未通过:审核中(0)、待审核(3)、未通过(2) + query = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + zhuangtai__in=[0, 2, 3] + ) + else: + query = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + zhuangtai=1 # 已通过 + ) + + total = query.count() + records = query.order_by('-create_time')[(page-1)*page_size : page*page_size] + + data = [] + for jl in records: + # 汇总缴纳总额 + total_fee = KaoheJiluFeiyong.objects.filter(jilu=jl).aggregate( + total=models.Sum('jine') + )['total'] or 0.00 + + # 上次拒绝原因(最近一条) + last_fail = KaoheJujueJilu.objects.filter(jilu=jl).order_by('-create_time').first() + fail_reason = last_fail.yuanyin if last_fail else '' + + # 考核官头像 + kg_user = UserMain.objects.filter(yonghuid=jl.shenheguan_id).first() + kg_avatar = kg_user.avatar if kg_user else '' + + data.append({ + 'jilu_id': jl.jilu_id, + 'chenghao_name': jl.chenghao.mingcheng if jl.chenghao else '', + 'bankuai_name': jl.bankuai.mingcheng if jl.bankuai else '', + 'zhuangtai': jl.zhuangtai, + 'statusText': dict(ShenheJilu._meta.get_field('zhuangtai').choices).get(jl.zhuangtai, '未知'), + 'statusColor': {0: '#FFA500', 1: '#27ae60', 2: '#e74c3c', 3: '#3498db'}.get(jl.zhuangtai, '#888'), + 'shenheguan_id': jl.shenheguan_id, + 'shenheguan_avatar': kg_avatar, + 'dashou_youxi_id': jl.dashou_youxi_id, + 'dashou_beizhu': jl.dashou_beizhu, + 'cishu': jl.cishu, + 'total_fee': float(total_fee), + 'last_fail_reason': fail_reason, + 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S'), + # 为再次申请预留标签对象数据(前端可能用到) + 'tag_obj': { + 'id': jl.chenghao_id, + 'name': jl.chenghao.mingcheng if jl.chenghao else '', + # 其他字段按需,但申请时会重新拉取 + } if jl.chenghao else None, + }) + + has_more = (page * page_size) < total + return Response({ + 'code': 0, + 'data': {'list': data, 'has_more': has_more, 'total': total} + }) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + + +from .utils import validate_shenheguan +class KaoheFreeApplyView(APIView): + """免费申请考核(首次/再次,费用为0时调用)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + tag_id = request.data.get('tag_id') + game_nick = request.data.get('game_nick', '').strip() + remark = request.data.get('remark', '').strip() + reapply = request.data.get('reapply', False) + jilu_id = request.data.get('jilu_id') if reapply else None + shenheguan_id = request.data.get('shenheguan_id', '').strip() # 新增 + + if not tag_id or not game_nick: + return Response({'code': 400, 'msg': '标签ID和游戏昵称必填'}) + + # 校验费用是否为0 + try: + chenghao = Chenghao.objects.get(id=tag_id) + except Chenghao.DoesNotExist: + return Response({'code': 400, 'msg': '标签不存在'}) + + # 计算本次次数 + if reapply and jilu_id: + last_record = ShenheJilu.objects.filter( + jilu_id=jilu_id, shenqingren_id=user.yonghuid, zhuangtai=2 + ).first() + if not last_record: + return Response({'code': 400, 'msg': '原记录不存在或状态不是未通过'}) + new_cishu = last_record.cishu + 1 + else: + max_cishu = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, chenghao=chenghao, zhuangtai__in=[1,2] + ).aggregate(max=models.Max('cishu'))['max'] or 0 + new_cishu = max_cishu + 1 + + fee = get_tag_fee(chenghao, new_cishu) + if fee > 0: + return Response({'code': 400, 'msg': '您有未通过的记录请查看并重考'}) + + # 新增:如果指定了考核官,进行合法性校验 + if shenheguan_id: + valid, err = validate_shenheguan(shenheguan_id, chenghao.bankuai, fee, user) + if not valid: + return Response({'code': 400, 'msg': err}) + + # 调用公共创建逻辑 + success, msg, record = create_shenhe_jilu( + user, tag_id, game_nick, remark, reapply, jilu_id, + shenheguan_id=shenheguan_id # 新增参数 + ) + if success: + return Response({'code': 0, 'msg': msg, 'data': {'jilu_id': record.jilu_id}}) + else: + return Response({'code': 400, 'msg': msg}) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + + + + +class KaoheJiluListView(APIView): + """ + 获取当前考核官的历史审核记录 + 路径:POST /dengji/khghqkhjl + 权限:JWT Token (考核官) + 参数: + bankuai_id : int (可选,0或空表示全部) + status : int (可选,0-全部 1-审核中 2-已通过 3-未通过) + page : int (默认1) + page_size : int (默认5) + 返回: + list, has_more, total + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + # 1. 验证考核官身份及账号状态 + kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() + if not kg: + return Response({'code': 403, 'msg': '您不是考核官或账号异常'}) + + # 2. 获取参数 + bankuai_id = request.data.get('bankuai_id') + status = int(request.data.get('status', 0)) # 0全部 1审核中 2已通过 3未通过 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 5)) + + # 3. 构建查询条件:只查当前考核官审核的记录 + base_query = ShenheJilu.objects.filter(shenheguan_id=yonghu.yonghuid) + + # 板块筛选 + if bankuai_id and int(bankuai_id) > 0: + base_query = base_query.filter(bankuai_id=int(bankuai_id)) + + # 状态筛选(前端传的状态映射到后端zhuangtai字段) + status_map = { + 0: None, # 全部不筛选 + 1: 0, # 审核中 + 2: 1, # 已通过 + 3: 2, # 未通过 + } + if status_map.get(status) is not None: + base_query = base_query.filter(zhuangtai=status_map[status]) + + # 4. 计数与分页 + total = base_query.count() + offset = (page - 1) * page_size + jilu_list = base_query.order_by('-create_time')[offset:offset + page_size] + + # 5. 预取关联数据,减少查询次数 + jilu_list = jilu_list.select_related('bankuai', 'chenghao') + # 为每条记录预取费用明细和拒绝记录(通过 related_name 或手动过滤) + # 由于 KaoheJiluFeiyong 和 KaoheJujueJilu 通过外键指向 ShenheJilu (to_field='jilu_id'), + # 可以使用 prefetch_related + jilu_ids = [j.jilu_id for j in jilu_list] + fee_dict = {} + reject_dict = {} + if jilu_ids: + # 批量获取所有费用明细 + fee_qs = KaoheJiluFeiyong.objects.filter(jilu_id__in=jilu_ids).order_by('cishu') + for fee in fee_qs: + if fee.jilu_id not in fee_dict: + fee_dict[fee.jilu_id] = [] + fee_dict[fee.jilu_id].append({'cishu': fee.cishu, 'jine': float(fee.jine)}) + + # 批量获取所有拒绝记录 + reject_qs = KaoheJujueJilu.objects.filter(jilu_id__in=jilu_ids).order_by('cishu') + for rej in reject_qs: + if rej.jilu_id not in reject_dict: + reject_dict[rej.jilu_id] = [] + reject_dict[rej.jilu_id].append({ + 'cishu': rej.cishu, + 'yuanyin': rej.yuanyin, + 'create_time': rej.create_time.strftime('%Y-%m-%d %H:%M:%S') if rej.create_time else '' + }) + + # 6. 批量获取申请人信息(昵称和头像) + applicant_ids = {j.shenqingren_id for j in jilu_list} + user_map = {} + if applicant_ids: + users = UserMain.objects.filter(yonghuid__in=applicant_ids).select_related('dashou_profile') + for u in users: + avatar = u.avatar or '' + nicheng = '' + try: + # 打手扩展表可能有 nicheng + nicheng = u.dashou_profile.nicheng if u.dashou_profile else '' + except UserDashou.DoesNotExist: + pass + user_map[u.yonghuid] = { + 'avatar': avatar, + 'nicheng': nicheng or f'用户{u.yonghuid}' + } + + # 7. 组装数据 + records = [] + for jl in jilu_list: + user_info = user_map.get(jl.shenqingren_id, {'avatar': '', 'nicheng': '未知'}) + # 标签特效JSON + texiao_json = {} + if jl.chenghao and jl.chenghao.texiao_miaoshu: + try: + texiao_json = json.loads(jl.chenghao.texiao_miaoshu) + except: + pass + + # 汇总总缴纳费用 + fee_items = fee_dict.get(jl.jilu_id, []) + total_fee = sum(item['jine'] for item in fee_items) + + records.append({ + 'jilu_id': jl.jilu_id, + 'shenqingren_id': jl.shenqingren_id, + 'shenqingren_avatar': user_info['avatar'], + 'shenqingren_nicheng': user_info['nicheng'], + 'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '', + 'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '', + 'chenghao_texiao_json': texiao_json, + 'zhuangtai': jl.zhuangtai, # 0审核中 1通过 2未通过 3待审核 + 'cishu': jl.cishu, + 'total_fee': total_fee, + 'dashou_youxi_id': jl.dashou_youxi_id or '', + 'dashou_beizhu': jl.dashou_beizhu or '', + 'shenheguan_youxi_id': jl.shenheguan_youxi_id or '', + 'guize_neirong': jl.guize_neirong or '', + 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S') if jl.create_time else '', + 'update_time': jl.update_time.strftime('%Y-%m-%d %H:%M:%S') if jl.update_time else '', + 'fee_list': fee_items, + 'reject_list': reject_dict.get(jl.jilu_id, []), + }) + + has_more = (page * page_size) < total + return Response({ + 'code': 0, + 'data': { + 'list': records, + 'has_more': has_more, + 'total': total + } + }) + + except Exception as e: + logger.error(f"获取考核记录异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) \ No newline at end of file diff --git a/dingdan/__init__.py b/dingdan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dingdan/__pycache__/__init__.cpython-313.pyc b/dingdan/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..7cb2d10 Binary files /dev/null and b/dingdan/__pycache__/__init__.cpython-313.pyc differ diff --git a/dingdan/__pycache__/admin.cpython-313.pyc b/dingdan/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000..0364cf6 Binary files /dev/null and b/dingdan/__pycache__/admin.cpython-313.pyc differ diff --git a/dingdan/__pycache__/apps.cpython-313.pyc b/dingdan/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000..d83aac8 Binary files /dev/null and b/dingdan/__pycache__/apps.cpython-313.pyc differ diff --git a/dingdan/__pycache__/models.cpython-312.pyc b/dingdan/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..3901c34 Binary files /dev/null and b/dingdan/__pycache__/models.cpython-312.pyc differ diff --git a/dingdan/__pycache__/models.cpython-313.pyc b/dingdan/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..73341ca Binary files /dev/null and b/dingdan/__pycache__/models.cpython-313.pyc differ diff --git a/dingdan/__pycache__/signals.cpython-313.pyc b/dingdan/__pycache__/signals.cpython-313.pyc new file mode 100644 index 0000000..a7e929b Binary files /dev/null and b/dingdan/__pycache__/signals.cpython-313.pyc differ diff --git a/dingdan/__pycache__/tasks.cpython-313.pyc b/dingdan/__pycache__/tasks.cpython-313.pyc new file mode 100644 index 0000000..6af6038 Binary files /dev/null and b/dingdan/__pycache__/tasks.cpython-313.pyc differ diff --git a/dingdan/__pycache__/urls.cpython-313.pyc b/dingdan/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..f9cd542 Binary files /dev/null and b/dingdan/__pycache__/urls.cpython-313.pyc differ diff --git a/dingdan/__pycache__/utils.cpython-312.pyc b/dingdan/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..dbe1e3c Binary files /dev/null and b/dingdan/__pycache__/utils.cpython-312.pyc differ diff --git a/dingdan/__pycache__/utils.cpython-313.pyc b/dingdan/__pycache__/utils.cpython-313.pyc new file mode 100644 index 0000000..3907b24 Binary files /dev/null and b/dingdan/__pycache__/utils.cpython-313.pyc differ diff --git a/dingdan/__pycache__/views.cpython-313.pyc b/dingdan/__pycache__/views.cpython-313.pyc new file mode 100644 index 0000000..71b453c Binary files /dev/null and b/dingdan/__pycache__/views.cpython-313.pyc differ diff --git a/dingdan/admin.py b/dingdan/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/dingdan/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/dingdan/apps.py b/dingdan/apps.py new file mode 100644 index 0000000..0a14712 --- /dev/null +++ b/dingdan/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig + + +class DingdanConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'dingdan' + + def ready(self): + # 导入信号处理,确保信号被注册 + import dingdan.signals \ No newline at end of file diff --git a/dingdan/cross_platform_tasks.py b/dingdan/cross_platform_tasks.py new file mode 100644 index 0000000..396dfe7 --- /dev/null +++ b/dingdan/cross_platform_tasks.py @@ -0,0 +1,43 @@ +# dingdan/cross_platform_tasks.py + +#不在使用 + + +import logging +import requests +from django.conf import settings + +logger = logging.getLogger(__name__) + +def send_partner_sync_order(partner_order_id, jine, dashou_fencheng, jieshao, beizhu, create_time,nicheng): + """ + 异步发送派单通知给对方平台(使用简单线程,非Celery) + 注意:此函数应在线程中调用,避免阻塞主请求。 + 生产环境建议使用Celery,此处提供线程兼容版本。 + """ + url = f"{settings.PARTNER_BASE_URL}/dingdan/partner_sync_order" + payload = { + 'token': settings.CROSS_PLATFORM_TOKEN, + 'partner_order_id': partner_order_id, + 'jine': jine, + 'dashou_fencheng': dashou_fencheng, + 'jieshao': jieshao, + 'beizhu': beizhu, + 'create_time': create_time, + 'nicheng': nicheng, + } + try: + response = requests.post(url, json=payload, timeout=5) + if response.status_code == 200: + data = response.json() + if data.get('code') == 0: + logger.info(f"跨平台派单同步成功: {partner_order_id}") + # 可在此处保存对方返回的 local_order_id 到本地订单(如果需要) + # local_order_id = data['data'].get('local_order_id') + # Dingdan.objects.filter(dingdan_id=partner_order_id).update(partner_order_id=local_order_id) + else: + logger.error(f"跨平台派单同步业务失败: {data}") + else: + logger.error(f"跨平台派单同步HTTP失败: {response.status_code}") + except Exception as e: + logger.error(f"跨平台派单同步异常: {str(e)}", exc_info=True) \ No newline at end of file diff --git a/dingdan/migrations/0001_initial.py b/dingdan/migrations/0001_initial.py new file mode 100644 index 0000000..2830185 --- /dev/null +++ b/dingdan/migrations/0001_initial.py @@ -0,0 +1,186 @@ +# Generated by Django 6.0 on 2025-12-20 23:54 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Dingdan", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "dingdan_id", + models.CharField(max_length=32, unique=True, verbose_name="订单ID"), + ), + ( + "zhuangtai", + models.IntegerField( + blank=True, default=1, null=True, verbose_name="订单状态" + ), + ), + ( + "fadan_pingtai", + models.IntegerField(blank=True, null=True, verbose_name="发单平台"), + ), + ( + "jine", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=10, + null=True, + verbose_name="订单金额", + ), + ), + ( + "dashou_fencheng", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=10, + null=True, + verbose_name="打手分成", + ), + ), + ( + "jiedan_dashou_id", + models.CharField( + blank=True, max_length=32, null=True, verbose_name="接单打手ID" + ), + ), + ( + "zhiding_id", + models.CharField( + blank=True, max_length=32, null=True, verbose_name="指定ID" + ), + ), + ( + "shangpin_id", + models.IntegerField(blank=True, null=True, verbose_name="商品ID"), + ), + ( + "jieshao", + models.TextField(blank=True, null=True, verbose_name="订单介绍"), + ), + ( + "beizhu", + models.TextField(blank=True, null=True, verbose_name="订单备注"), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "verbose_name": "订单", + "verbose_name_plural": "订单", + "db_table": "dingdan", + "indexes": [ + models.Index( + fields=["zhuangtai"], name="dingdan_zhuangt_2febe8_idx" + ), + models.Index( + fields=["dingdan_id"], name="dingdan_dingdan_83dcb6_idx" + ), + models.Index( + fields=["jiedan_dashou_id"], name="dingdan_jiedan__921bfa_idx" + ), + ], + }, + ), + migrations.CreateModel( + name="DingdanPingtai", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "laoban_id", + models.CharField( + blank=True, max_length=32, null=True, verbose_name="老板ID" + ), + ), + ( + "laoban_pingjia", + models.TextField(blank=True, null=True, verbose_name="老板评价"), + ), + ( + "dingdan", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="pingtai_kuozhan", + to="dingdan.dingdan", + verbose_name="订单", + ), + ), + ], + options={ + "verbose_name": "平台订单扩展", + "verbose_name_plural": "平台订单扩展", + "db_table": "dingdan_pingtai", + }, + ), + migrations.CreateModel( + name="DingdanShangjia", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "shangjia_id", + models.CharField( + blank=True, max_length=32, null=True, verbose_name="商家ID" + ), + ), + ( + "shangjia_pingjia", + models.TextField(blank=True, null=True, verbose_name="商家评价"), + ), + ( + "dingdan", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="shangjia_kuozhan", + to="dingdan.dingdan", + verbose_name="订单", + ), + ), + ], + options={ + "verbose_name": "商家订单扩展", + "verbose_name_plural": "商家订单扩展", + "db_table": "dingdan_shangjia", + }, + ), + ] diff --git a/dingdan/migrations/0002_dingdan_huiyuan_id_dingdan_yaoqiuleixing_and_more.py b/dingdan/migrations/0002_dingdan_huiyuan_id_dingdan_yaoqiuleixing_and_more.py new file mode 100644 index 0000000..9ae6e37 --- /dev/null +++ b/dingdan/migrations/0002_dingdan_huiyuan_id_dingdan_yaoqiuleixing_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 6.0 on 2025-12-23 21:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="dingdan", + name="huiyuan_id", + field=models.CharField( + blank=True, max_length=6, null=True, verbose_name="会员ID" + ), + ), + migrations.AddField( + model_name="dingdan", + name="yaoqiuleixing", + field=models.PositiveIntegerField( + blank=True, null=True, verbose_name="抢单要求类型" + ), + ), + migrations.AddField( + model_name="dingdan", + name="yongjin", + field=models.DecimalField( + blank=True, + decimal_places=2, + max_digits=10, + null=True, + verbose_name="佣金要求", + ), + ), + ] diff --git a/dingdan/migrations/0003_lilubiao.py b/dingdan/migrations/0003_lilubiao.py new file mode 100644 index 0000000..2578a70 --- /dev/null +++ b/dingdan/migrations/0003_lilubiao.py @@ -0,0 +1,55 @@ +# Generated by Django 6.0 on 2025-12-23 21:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0002_dingdan_huiyuan_id_dingdan_yaoqiuleixing_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Lilubiao", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "lilu", + models.DecimalField( + decimal_places=4, + max_digits=5, + null=True, + verbose_name="平台分成利率,1是平台,2是商家", + ), + ), + ( + "fadanpingtai", + models.CharField( + max_length=10, + null=True, + verbose_name="订单所属平台,(商家或平台)", + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "db_table": "lilubiao", + }, + ), + ] diff --git a/dingdan/migrations/0004_dingdan_nicheng.py b/dingdan/migrations/0004_dingdan_nicheng.py new file mode 100644 index 0000000..3b1b0d0 --- /dev/null +++ b/dingdan/migrations/0004_dingdan_nicheng.py @@ -0,0 +1,20 @@ +# Generated by Django 6.0 on 2025-12-24 23:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0003_lilubiao"), + ] + + operations = [ + migrations.AddField( + model_name="dingdan", + name="nicheng", + field=models.CharField( + blank=True, max_length=50, null=True, verbose_name="游戏昵称" + ), + ), + ] diff --git a/dingdan/migrations/0005_dingdan_tupian.py b/dingdan/migrations/0005_dingdan_tupian.py new file mode 100644 index 0000000..00833c9 --- /dev/null +++ b/dingdan/migrations/0005_dingdan_tupian.py @@ -0,0 +1,20 @@ +# Generated by Django 6.0 on 2025-12-25 22:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0004_dingdan_nicheng"), + ] + + operations = [ + migrations.AddField( + model_name="dingdan", + name="tupian", + field=models.CharField( + blank=True, max_length=500, null=True, verbose_name="商品图片URL" + ), + ), + ] diff --git a/dingdan/migrations/0006_dingdan_dashou_liuyan_dingdan_leixing_id_and_more.py b/dingdan/migrations/0006_dingdan_dashou_liuyan_dingdan_leixing_id_and_more.py new file mode 100644 index 0000000..8021048 --- /dev/null +++ b/dingdan/migrations/0006_dingdan_dashou_liuyan_dingdan_leixing_id_and_more.py @@ -0,0 +1,157 @@ +# Generated by Django 6.0 on 2025-12-26 17:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0005_dingdan_tupian"), + ] + + operations = [ + migrations.AddField( + model_name="dingdan", + name="dashou_liuyan", + field=models.TextField(blank=True, null=True, verbose_name="打手留言"), + ), + migrations.AddField( + model_name="dingdan", + name="leixing_id", + field=models.PositiveIntegerField( + blank=True, null=True, verbose_name="商品类型ID" + ), + ), + migrations.AlterField( + model_name="dingdan", + name="dingdan_id", + field=models.CharField( + db_index=True, max_length=32, unique=True, verbose_name="订单ID" + ), + ), + migrations.AlterField( + model_name="dingdan", + name="jiedan_dashou_id", + field=models.CharField( + blank=True, + db_index=True, + max_length=32, + null=True, + verbose_name="接单打手ID", + ), + ), + migrations.AlterField( + model_name="dingdan", + name="zhuangtai", + field=models.IntegerField( + blank=True, db_index=True, default=1, null=True, verbose_name="订单状态" + ), + ), + migrations.CreateModel( + name="Dashoutupian", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "dingdan_id", + models.CharField( + db_index=True, max_length=32, null=True, verbose_name="订单ID" + ), + ), + ( + "dashou", + models.CharField( + blank=True, + db_index=True, + max_length=32, + null=True, + verbose_name="接单打手ID", + ), + ), + ( + "tupian", + models.CharField( + blank=True, + max_length=500, + null=True, + verbose_name="打手提交图片URL", + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "db_table": "dashoutupian", + "indexes": [ + models.Index( + fields=["dashou"], name="dashoutupia_dashou_7de8bd_idx" + ), + models.Index( + fields=["dingdan_id"], name="dashoutupia_dingdan_97c685_idx" + ), + ], + }, + ), + migrations.CreateModel( + name="Pingfen", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "dashou", + models.CharField( + blank=True, + db_index=True, + max_length=32, + null=True, + verbose_name="接单打手ID", + ), + ), + ( + "zongfen", + models.PositiveIntegerField(default=0, verbose_name="总分"), + ), + ( + "cishu", + models.PositiveIntegerField(default=0, verbose_name="评分次数"), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "verbose_name": "评分表", + "db_table": "pingfenbiao", + "indexes": [ + models.Index( + fields=["dashou"], name="pingfenbiao_dashou_f62f10_idx" + ) + ], + }, + ), + ] diff --git a/dingdan/migrations/0007_chufajilu_tuikuanjilu_dingdanshangjia_bhliyou_and_more.py b/dingdan/migrations/0007_chufajilu_tuikuanjilu_dingdanshangjia_bhliyou_and_more.py new file mode 100644 index 0000000..52bf9e8 --- /dev/null +++ b/dingdan/migrations/0007_chufajilu_tuikuanjilu_dingdanshangjia_bhliyou_and_more.py @@ -0,0 +1,280 @@ +# Generated by Django 6.0 on 2025-12-30 15:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0006_dingdan_dashou_liuyan_dingdan_leixing_id_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Chufajilu", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "dashouid", + models.CharField( + db_index=True, + max_length=7, + null=True, + verbose_name="被罚打手ID", + ), + ), + ( + "qingqiuid", + models.CharField( + db_index=True, + max_length=7, + null=True, + verbose_name="请求处罚ID", + ), + ), + ( + "chuliid", + models.CharField( + db_index=True, + max_length=7, + null=True, + verbose_name="处理处罚ID", + ), + ), + ( + "cfliyou", + models.TextField(blank=True, null=True, verbose_name="处罚理由"), + ), + ( + "sqzhuangtai", + models.IntegerField( + blank=True, + default=0, + null=True, + verbose_name="处罚申请状态:0待处理,1成功,2驳回", + ), + ), + ( + "bhliyou", + models.TextField( + blank=True, null=True, verbose_name="驳回处罚理由" + ), + ), + ("jifen", models.IntegerField(default=0, verbose_name="扣除打手积分")), + ( + "dingdan_id", + models.CharField( + db_index=True, max_length=32, null=True, verbose_name="订单ID" + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "db_table": "chufajilu", + }, + ), + migrations.CreateModel( + name="Tuikuanjilu", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "dingdan_id", + models.CharField(max_length=32, unique=True, verbose_name="订单ID"), + ), + ( + "dashouid", + models.CharField( + max_length=7, null=True, verbose_name="接单打手ID" + ), + ), + ( + "qingqiuid", + models.CharField( + max_length=7, null=True, verbose_name="请求用户ID" + ), + ), + ( + "chuliid", + models.CharField( + db_index=True, max_length=7, null=True, verbose_name="处理人ID" + ), + ), + ( + "cfliyou", + models.TextField(blank=True, null=True, verbose_name="理由"), + ), + ( + "sqzhuangtai", + models.IntegerField( + blank=True, + db_index=True, + default=0, + null=True, + verbose_name="处罚申请状态:0待处理,1成功,2驳回", + ), + ), + ( + "bhliyou", + models.TextField(blank=True, null=True, verbose_name="驳回理由"), + ), + ( + "jine", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=10, + null=True, + verbose_name="订单金额", + ), + ), + ( + "dashou_fencheng", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=10, + null=True, + verbose_name="打手分成", + ), + ), + ( + "jieshao", + models.TextField(blank=True, null=True, verbose_name="订单介绍"), + ), + ( + "beizhu", + models.TextField(blank=True, null=True, verbose_name="订单备注"), + ), + ( + "nicheng", + models.CharField( + blank=True, max_length=50, null=True, verbose_name="游戏昵称" + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "db_table": "tuikuanjilu", + }, + ), + migrations.AddField( + model_name="dingdanshangjia", + name="bhliyou", + field=models.TextField( + blank=True, null=True, verbose_name="驳回商家处罚理由" + ), + ), + migrations.AddField( + model_name="dingdanshangjia", + name="cfliyou", + field=models.TextField(blank=True, null=True, verbose_name="商家处罚理由"), + ), + migrations.AddField( + model_name="dingdanshangjia", + name="sqzhuangtai", + field=models.IntegerField( + blank=True, + default=0, + null=True, + verbose_name="处罚申请状态:0待处理,1成功,2驳回", + ), + ), + migrations.AlterField( + model_name="dingdanpingtai", + name="laoban_id", + field=models.CharField( + blank=True, + db_index=True, + max_length=32, + null=True, + verbose_name="老板ID", + ), + ), + migrations.AlterField( + model_name="dingdanshangjia", + name="shangjia_id", + field=models.CharField( + blank=True, + db_index=True, + max_length=32, + null=True, + verbose_name="商家ID", + ), + ), + migrations.AddIndex( + model_name="dingdanpingtai", + index=models.Index( + fields=["laoban_id"], name="dingdan_pin_laoban__d55e6f_idx" + ), + ), + migrations.AddIndex( + model_name="dingdanshangjia", + index=models.Index( + fields=["shangjia_id"], name="dingdan_sha_shangji_64d28b_idx" + ), + ), + migrations.AddIndex( + model_name="chufajilu", + index=models.Index( + fields=["dashouid"], name="chufajilu_dashoui_07aac0_idx" + ), + ), + migrations.AddIndex( + model_name="chufajilu", + index=models.Index( + fields=["qingqiuid"], name="chufajilu_qingqiu_ac160e_idx" + ), + ), + migrations.AddIndex( + model_name="chufajilu", + index=models.Index(fields=["chuliid"], name="chufajilu_chuliid_cd63ac_idx"), + ), + migrations.AddIndex( + model_name="chufajilu", + index=models.Index( + fields=["dingdan_id"], name="chufajilu_dingdan_b8f0c2_idx" + ), + ), + migrations.AddIndex( + model_name="tuikuanjilu", + index=models.Index( + fields=["chuliid"], name="tuikuanjilu_chuliid_0b0f16_idx" + ), + ), + migrations.AddIndex( + model_name="tuikuanjilu", + index=models.Index( + fields=["sqzhuangtai"], name="tuikuanjilu_sqzhuan_d89dc9_idx" + ), + ), + ] diff --git a/dingdan/migrations/0008_ptliushui_rename_cfliyou_tuikuanjilu_liyou_and_more.py b/dingdan/migrations/0008_ptliushui_rename_cfliyou_tuikuanjilu_liyou_and_more.py new file mode 100644 index 0000000..54edc2e --- /dev/null +++ b/dingdan/migrations/0008_ptliushui_rename_cfliyou_tuikuanjilu_liyou_and_more.py @@ -0,0 +1,122 @@ +# Generated by Django 6.0 on 2025-12-30 17:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0007_chufajilu_tuikuanjilu_dingdanshangjia_bhliyou_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Ptliushui", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "zonge", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=10, + null=True, + verbose_name="收取总金额", + ), + ), + ( + "zhichuzonge", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=10, + null=True, + verbose_name="支出总金额", + ), + ), + ( + "zongshouyi", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=10, + null=True, + verbose_name="总收益", + ), + ), + ( + "rizonge", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=10, + null=True, + verbose_name="日收取", + ), + ), + ( + "rizhichu", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=10, + null=True, + verbose_name="日支出", + ), + ), + ( + "rishouyi", + models.DecimalField( + blank=True, + decimal_places=2, + max_digits=10, + null=True, + verbose_name="日收益", + ), + ), + ( + "zongshoucishu", + models.IntegerField(default=0, verbose_name="总收取次数"), + ), + ( + "rishoucishu", + models.IntegerField(default=0, verbose_name="日收取次数"), + ), + ( + "zongzhichucishu", + models.IntegerField(default=0, verbose_name="总支出次数"), + ), + ( + "rizhichucishu", + models.IntegerField(default=0, verbose_name="日支出次数"), + ), + ], + options={ + "db_table": "ptliushui", + }, + ), + migrations.RenameField( + model_name="tuikuanjilu", + old_name="cfliyou", + new_name="liyou", + ), + migrations.AlterField( + model_name="tuikuanjilu", + name="sqzhuangtai", + field=models.IntegerField( + blank=True, + db_index=True, + default=0, + null=True, + verbose_name="申请状态:0待处理,1成功,2驳回", + ), + ), + ] diff --git a/dingdan/migrations/0009_dingdan_liaotian_pindao.py b/dingdan/migrations/0009_dingdan_liaotian_pindao.py new file mode 100644 index 0000000..6bd4ba2 --- /dev/null +++ b/dingdan/migrations/0009_dingdan_liaotian_pindao.py @@ -0,0 +1,20 @@ +# Generated by Django 6.0 on 2025-12-31 11:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0008_ptliushui_rename_cfliyou_tuikuanjilu_liyou_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="dingdan", + name="liaotian_pindao", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="聊天频道名" + ), + ), + ] diff --git a/dingdan/migrations/0010_remove_dingdan_liaotian_pindao_dingdan_user1_id_and_more.py b/dingdan/migrations/0010_remove_dingdan_liaotian_pindao_dingdan_user1_id_and_more.py new file mode 100644 index 0000000..1b3d875 --- /dev/null +++ b/dingdan/migrations/0010_remove_dingdan_liaotian_pindao_dingdan_user1_id_and_more.py @@ -0,0 +1,89 @@ +# Generated by Django 6.0 on 2025-12-31 15:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0009_dingdan_liaotian_pindao"), + ] + + operations = [ + migrations.RemoveField( + model_name="dingdan", + name="liaotian_pindao", + ), + migrations.AddField( + model_name="dingdan", + name="user1_id", + field=models.CharField( + db_index=True, max_length=30, null=True, verbose_name="用户1标识" + ), + ), + migrations.AddField( + model_name="dingdan", + name="user2_id", + field=models.CharField( + db_index=True, max_length=30, null=True, verbose_name="用户2打手标识" + ), + ), + migrations.CreateModel( + name="Liaotian", + fields=[ + ( + "id", + models.BigAutoField( + primary_key=True, serialize=False, verbose_name="主键ID" + ), + ), + ( + "user1_id", + models.CharField( + db_index=True, max_length=30, verbose_name="用户1标识" + ), + ), + ( + "user2_id", + models.CharField( + db_index=True, max_length=30, verbose_name="用户2标识" + ), + ), + ( + "dingdan_id", + models.CharField(max_length=32, unique=True, verbose_name="订单ID"), + ), + ( + "zhuangtai", + models.IntegerField( + choices=[(1, "开启中"), (0, "已关闭")], + db_index=True, + default=1, + verbose_name="聊天状态", + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "db_table": "liaotian", + "indexes": [ + models.Index( + fields=["user1_id"], name="liaotian_user1_i_6be6cd_idx" + ), + models.Index( + fields=["user2_id"], name="liaotian_user2_i_9f61a4_idx" + ), + models.Index( + fields=["zhuangtai"], name="liaotian_zhuangt_5e8be5_idx" + ), + ], + }, + ), + ] diff --git a/dingdan/migrations/0011_dingdanshangjia_sjnicheng.py b/dingdan/migrations/0011_dingdanshangjia_sjnicheng.py new file mode 100644 index 0000000..61eaea2 --- /dev/null +++ b/dingdan/migrations/0011_dingdanshangjia_sjnicheng.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0 on 2026-01-01 00:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0010_remove_dingdan_liaotian_pindao_dingdan_user1_id_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="dingdanshangjia", + name="sjnicheng", + field=models.CharField(max_length=50, null=True, verbose_name="商家昵称"), + ), + ] diff --git a/dingdan/migrations/0012_dingdan_auto_expire_at_dingdan_auto_task_id_and_more.py b/dingdan/migrations/0012_dingdan_auto_expire_at_dingdan_auto_task_id_and_more.py new file mode 100644 index 0000000..413bf2d --- /dev/null +++ b/dingdan/migrations/0012_dingdan_auto_expire_at_dingdan_auto_task_id_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.27 on 2026-01-20 15:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0011_dingdanshangjia_sjnicheng"), + ] + + operations = [ + migrations.AddField( + model_name="dingdan", + name="auto_expire_at", + field=models.DateTimeField( + blank=True, null=True, verbose_name="计划过期时间" + ), + ), + migrations.AddField( + model_name="dingdan", + name="auto_task_id", + field=models.CharField( + blank=True, default="", max_length=255, verbose_name="延时任务ID" + ), + ), + migrations.AddField( + model_name="dingdan", + name="pending_dispatch", + field=models.BooleanField( + db_index=True, default=False, verbose_name="待调度标记" + ), + ), + migrations.AddField( + model_name="dingdan", + name="status_8_time", + field=models.DateTimeField( + blank=True, null=True, verbose_name="状态变为8的时间" + ), + ), + ] diff --git a/dingdan/migrations/0013_alter_chufajilu_chuliid.py b/dingdan/migrations/0013_alter_chufajilu_chuliid.py new file mode 100644 index 0000000..5466ff9 --- /dev/null +++ b/dingdan/migrations/0013_alter_chufajilu_chuliid.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.27 on 2026-01-21 11:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0012_dingdan_auto_expire_at_dingdan_auto_task_id_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="chufajilu", + name="chuliid", + field=models.CharField( + db_index=True, max_length=11, null=True, verbose_name="处理处罚ID" + ), + ), + ] diff --git a/dingdan/migrations/0014_chufajilu_ssliyou_alter_chufajilu_sqzhuangtai_and_more.py b/dingdan/migrations/0014_chufajilu_ssliyou_alter_chufajilu_sqzhuangtai_and_more.py new file mode 100644 index 0000000..6bcfe0a --- /dev/null +++ b/dingdan/migrations/0014_chufajilu_ssliyou_alter_chufajilu_sqzhuangtai_and_more.py @@ -0,0 +1,96 @@ +# Generated by Django 4.2.27 on 2026-02-06 15:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0013_alter_chufajilu_chuliid"), + ] + + operations = [ + migrations.AddField( + model_name="chufajilu", + name="ssliyou", + field=models.TextField(blank=True, null=True, verbose_name="打手申诉理由"), + ), + migrations.AlterField( + model_name="chufajilu", + name="sqzhuangtai", + field=models.IntegerField( + blank=True, + default=0, + null=True, + verbose_name="处罚申请状态:0待处理,1成功,2驳回,3申诉", + ), + ), + migrations.AlterField( + model_name="lilubiao", + name="lilu", + field=models.DecimalField( + decimal_places=4, + max_digits=5, + null=True, + verbose_name="平台分成利率,1是平台,3是商家", + ), + ), + migrations.CreateModel( + name="Chufatupian", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "dingdan_id", + models.CharField( + db_index=True, max_length=32, null=True, verbose_name="订单ID" + ), + ), + ( + "yonghuid", + models.CharField( + blank=True, + db_index=True, + max_length=32, + null=True, + verbose_name="用户ID", + ), + ), + ( + "tupian", + models.CharField( + blank=True, + max_length=500, + null=True, + verbose_name="证据图片URL", + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "db_table": "chufatupian", + "indexes": [ + models.Index( + fields=["yonghuid"], name="chufatupian_yonghui_71e585_idx" + ), + models.Index( + fields=["dingdan_id"], name="chufatupian_dingdan_ce0dd4_idx" + ), + ], + }, + ), + ] diff --git a/dingdan/migrations/0015_dingdan_tkly.py b/dingdan/migrations/0015_dingdan_tkly.py new file mode 100644 index 0000000..100d464 --- /dev/null +++ b/dingdan/migrations/0015_dingdan_tkly.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.27 on 2026-02-13 22:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0014_chufajilu_ssliyou_alter_chufajilu_sqzhuangtai_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="dingdan", + name="tkly", + field=models.CharField( + blank=True, max_length=500, null=True, verbose_name="退款/拒绝理由" + ), + ), + ] diff --git a/dingdan/migrations/0016_dingdan_is_cross_dingdan_partner_order_id.py b/dingdan/migrations/0016_dingdan_is_cross_dingdan_partner_order_id.py new file mode 100644 index 0000000..39b946b --- /dev/null +++ b/dingdan/migrations/0016_dingdan_is_cross_dingdan_partner_order_id.py @@ -0,0 +1,31 @@ +# Generated by Django 4.2.27 on 2026-02-19 00:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0015_dingdan_tkly"), + ] + + operations = [ + migrations.AddField( + model_name="dingdan", + name="is_cross", + field=models.IntegerField( + default=0, verbose_name="是否跨平台订单(0否,1是)" + ), + ), + migrations.AddField( + model_name="dingdan", + name="partner_order_id", + field=models.CharField( + blank=True, + db_index=True, + max_length=32, + null=True, + verbose_name="对方平台订单ID", + ), + ), + ] diff --git a/dingdan/migrations/0017_dingdan_clkf_dingdan_dingdan_clkf_6793b1_idx.py b/dingdan/migrations/0017_dingdan_clkf_dingdan_dingdan_clkf_6793b1_idx.py new file mode 100644 index 0000000..551bd20 --- /dev/null +++ b/dingdan/migrations/0017_dingdan_clkf_dingdan_dingdan_clkf_6793b1_idx.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.27 on 2026-02-19 23:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0016_dingdan_is_cross_dingdan_partner_order_id"), + ] + + operations = [ + migrations.AddField( + model_name="dingdan", + name="clkf", + field=models.CharField( + db_index=True, max_length=20, null=True, verbose_name="处理客服" + ), + ), + migrations.AddIndex( + model_name="dingdan", + index=models.Index(fields=["clkf"], name="dingdan_clkf_6793b1_idx"), + ), + ] diff --git a/dingdan/migrations/0018_dingdanpingtai_jjjs_biaoshi.py b/dingdan/migrations/0018_dingdanpingtai_jjjs_biaoshi.py new file mode 100644 index 0000000..16741b3 --- /dev/null +++ b/dingdan/migrations/0018_dingdanpingtai_jjjs_biaoshi.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.27 on 2026-02-20 17:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0017_dingdan_clkf_dingdan_dingdan_clkf_6793b1_idx"), + ] + + operations = [ + migrations.AddField( + model_name="dingdanpingtai", + name="jjjs_biaoshi", + field=models.IntegerField( + default=0, verbose_name="拒绝结算标记(1表示拒绝结算)" + ), + ), + ] diff --git a/dingdan/migrations/0019_dingdan_wechat_transaction_id_and_more.py b/dingdan/migrations/0019_dingdan_wechat_transaction_id_and_more.py new file mode 100644 index 0000000..2da9b7f --- /dev/null +++ b/dingdan/migrations/0019_dingdan_wechat_transaction_id_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.27 on 2026-03-01 23:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0018_dingdanpingtai_jjjs_biaoshi"), + ] + + operations = [ + migrations.AddField( + model_name="dingdan", + name="wechat_transaction_id", + field=models.CharField( + blank=True, + db_index=True, + max_length=64, + null=True, + verbose_name="微信支付订单号", + ), + ), + migrations.AddIndex( + model_name="dingdan", + index=models.Index( + fields=["wechat_transaction_id"], name="dingdan_wechat__278b79_idx" + ), + ), + ] diff --git a/dingdan/migrations/0020_dingdan_dispatch_type_dingdan_partner_club_id.py b/dingdan/migrations/0020_dingdan_dispatch_type_dingdan_partner_club_id.py new file mode 100644 index 0000000..e220e27 --- /dev/null +++ b/dingdan/migrations/0020_dingdan_dispatch_type_dingdan_partner_club_id.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.27 on 2026-03-30 12:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0019_dingdan_wechat_transaction_id_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="dingdan", + name="dispatch_type", + field=models.IntegerField( + choices=[(1, "我方派单"), (2, "对方派单")], + default=0, + verbose_name="派发情况", + ), + ), + migrations.AddField( + model_name="dingdan", + name="partner_club_id", + field=models.CharField( + blank=True, + db_index=True, + max_length=6, + null=True, + verbose_name="对方俱乐部ID", + ), + ), + ] diff --git a/dingdan/migrations/0021_presettlement_orderdashouhistory_and_more.py b/dingdan/migrations/0021_presettlement_orderdashouhistory_and_more.py new file mode 100644 index 0000000..7b429d1 --- /dev/null +++ b/dingdan/migrations/0021_presettlement_orderdashouhistory_and_more.py @@ -0,0 +1,257 @@ +# Generated by Django 4.2.27 on 2026-04-01 00:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0020_dingdan_dispatch_type_dingdan_partner_club_id"), + ] + + operations = [ + migrations.CreateModel( + name="PreSettlement", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "dingdan_id", + models.CharField( + db_index=True, + max_length=32, + unique=True, + verbose_name="我方订单ID", + ), + ), + ( + "partner_order_id", + models.CharField( + db_index=True, max_length=32, verbose_name="对方订单ID" + ), + ), + ( + "partner_club_id", + models.CharField( + db_index=True, max_length=6, verbose_name="对方俱乐部ID" + ), + ), + ( + "dashou_id", + models.CharField( + db_index=True, max_length=7, verbose_name="接单打手ID" + ), + ), + ( + "amount", + models.DecimalField( + decimal_places=2, max_digits=10, verbose_name="打手应分金额" + ), + ), + ( + "order_amount", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=10, + verbose_name="订单原金额", + ), + ), + ( + "status", + models.SmallIntegerField( + choices=[(0, "待结算"), (1, "已结算"), (2, "已拒绝")], + default=0, + verbose_name="结算状态", + ), + ), + ( + "process_time", + models.DateTimeField( + blank=True, null=True, verbose_name="处理时间" + ), + ), + ( + "operator", + models.CharField( + blank=True, default="", max_length=32, verbose_name="操作人" + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "verbose_name": "预结算表", + "verbose_name_plural": "预结算表", + "db_table": "pre_settlement", + "indexes": [ + models.Index( + fields=["dingdan_id"], name="pre_settlem_dingdan_4f3541_idx" + ), + models.Index( + fields=["partner_order_id"], + name="pre_settlem_partner_45b29e_idx", + ), + models.Index( + fields=["partner_club_id"], + name="pre_settlem_partner_d2c44c_idx", + ), + models.Index( + fields=["dashou_id"], name="pre_settlem_dashou__d5fe7c_idx" + ), + models.Index( + fields=["status"], name="pre_settlem_status_0aa711_idx" + ), + ], + }, + ), + migrations.CreateModel( + name="OrderDashouHistory", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "dingdan_id", + models.CharField( + db_index=True, max_length=32, verbose_name="订单ID" + ), + ), + ( + "dashou_id", + models.CharField( + db_index=True, max_length=7, verbose_name="打手ID" + ), + ), + ( + "times", + models.PositiveSmallIntegerField( + verbose_name="第几次接单(从1开始)" + ), + ), + ( + "operator", + models.CharField(max_length=11, verbose_name="操作客服ID"), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ], + options={ + "verbose_name": "订单打手变更历史", + "verbose_name_plural": "订单打手变更历史", + "db_table": "order_dashou_history", + "indexes": [ + models.Index( + fields=["dingdan_id"], name="order_dasho_dingdan_1e79ab_idx" + ), + models.Index( + fields=["dashou_id"], name="order_dasho_dashou__0a637c_idx" + ), + ], + }, + ), + migrations.CreateModel( + name="CrossPlatformOrderData", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "dingdan_id", + models.CharField( + db_index=True, + max_length=32, + unique=True, + verbose_name="我方订单ID", + ), + ), + ( + "partner_order_id", + models.CharField( + db_index=True, max_length=32, verbose_name="对方平台订单ID" + ), + ), + ( + "partner_club_id", + models.CharField( + db_index=True, max_length=6, verbose_name="对方俱乐部ID" + ), + ), + ( + "partner_order_status", + models.IntegerField(default=0, verbose_name="对方平台订单状态"), + ), + ( + "partner_dashou_id", + models.CharField( + blank=True, + default="", + max_length=32, + verbose_name="对方平台接单打手ID", + ), + ), + ( + "partner_amount", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=10, + verbose_name="对方平台订单金额", + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "verbose_name": "跨平台订单扩展", + "verbose_name_plural": "跨平台订单扩展", + "db_table": "cross_platform_order_data", + "indexes": [ + models.Index( + fields=["dingdan_id"], name="cross_platf_dingdan_498d08_idx" + ), + models.Index( + fields=["partner_order_id"], + name="cross_platf_partner_5d5615_idx", + ), + models.Index( + fields=["partner_club_id"], + name="cross_platf_partner_1054e1_idx", + ), + ], + }, + ), + ] diff --git a/dingdan/migrations/0022_alter_presettlement_dingdan_id_and_more.py b/dingdan/migrations/0022_alter_presettlement_dingdan_id_and_more.py new file mode 100644 index 0000000..1b94e84 --- /dev/null +++ b/dingdan/migrations/0022_alter_presettlement_dingdan_id_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.27 on 2026-04-06 23:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0021_presettlement_orderdashouhistory_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="presettlement", + name="dingdan_id", + field=models.CharField( + db_index=True, max_length=32, verbose_name="我方订单ID" + ), + ), + migrations.AlterUniqueTogether( + name="presettlement", + unique_together={("dingdan_id", "dashou_id")}, + ), + ] diff --git a/dingdan/migrations/0023_alter_lilubiao_fadanpingtai.py b/dingdan/migrations/0023_alter_lilubiao_fadanpingtai.py new file mode 100644 index 0000000..15749ff --- /dev/null +++ b/dingdan/migrations/0023_alter_lilubiao_fadanpingtai.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.27 on 2026-04-10 18:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0022_alter_presettlement_dingdan_id_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="lilubiao", + name="fadanpingtai", + field=models.CharField( + max_length=10, + null=True, + verbose_name="利率类型,平台分成利率,1是平台订单打手分成,3是商家订单打手分成,5:打手提现佣金抽成6:管事提现抽成,8:组长提现抽成", + ), + ), + ] diff --git a/dingdan/migrations/0024_dingdan_guanshi_fencheng.py b/dingdan/migrations/0024_dingdan_guanshi_fencheng.py new file mode 100644 index 0000000..c294cc0 --- /dev/null +++ b/dingdan/migrations/0024_dingdan_guanshi_fencheng.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.27 on 2026-06-07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dingdan", "0023_alter_lilubiao_fadanpingtai"), + ] + + operations = [ + migrations.AddField( + model_name="dingdan", + name="guanshi_fencheng", + field=models.DecimalField( + blank=True, + decimal_places=2, + default=0, + max_digits=10, + null=True, + verbose_name="管事分成", + ), + ), + ] diff --git a/dingdan/migrations/__init__.py b/dingdan/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dingdan/migrations/__pycache__/0001_initial.cpython-313.pyc b/dingdan/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000..8e28b1e Binary files /dev/null and b/dingdan/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0002_dingdan_huiyuan_id_dingdan_yaoqiuleixing_and_more.cpython-313.pyc b/dingdan/migrations/__pycache__/0002_dingdan_huiyuan_id_dingdan_yaoqiuleixing_and_more.cpython-313.pyc new file mode 100644 index 0000000..78b5808 Binary files /dev/null and b/dingdan/migrations/__pycache__/0002_dingdan_huiyuan_id_dingdan_yaoqiuleixing_and_more.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0003_lilubiao.cpython-313.pyc b/dingdan/migrations/__pycache__/0003_lilubiao.cpython-313.pyc new file mode 100644 index 0000000..d4ea504 Binary files /dev/null and b/dingdan/migrations/__pycache__/0003_lilubiao.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0004_dingdan_nicheng.cpython-313.pyc b/dingdan/migrations/__pycache__/0004_dingdan_nicheng.cpython-313.pyc new file mode 100644 index 0000000..4e2e517 Binary files /dev/null and b/dingdan/migrations/__pycache__/0004_dingdan_nicheng.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0005_dingdan_tupian.cpython-313.pyc b/dingdan/migrations/__pycache__/0005_dingdan_tupian.cpython-313.pyc new file mode 100644 index 0000000..b1f398c Binary files /dev/null and b/dingdan/migrations/__pycache__/0005_dingdan_tupian.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0006_dingdan_dashou_liuyan_dingdan_leixing_id_and_more.cpython-313.pyc b/dingdan/migrations/__pycache__/0006_dingdan_dashou_liuyan_dingdan_leixing_id_and_more.cpython-313.pyc new file mode 100644 index 0000000..8ebc0e2 Binary files /dev/null and b/dingdan/migrations/__pycache__/0006_dingdan_dashou_liuyan_dingdan_leixing_id_and_more.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0007_chufajilu_tuikuanjilu_dingdanshangjia_bhliyou_and_more.cpython-313.pyc b/dingdan/migrations/__pycache__/0007_chufajilu_tuikuanjilu_dingdanshangjia_bhliyou_and_more.cpython-313.pyc new file mode 100644 index 0000000..b4ca80b Binary files /dev/null and b/dingdan/migrations/__pycache__/0007_chufajilu_tuikuanjilu_dingdanshangjia_bhliyou_and_more.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0008_ptliushui_rename_cfliyou_tuikuanjilu_liyou_and_more.cpython-313.pyc b/dingdan/migrations/__pycache__/0008_ptliushui_rename_cfliyou_tuikuanjilu_liyou_and_more.cpython-313.pyc new file mode 100644 index 0000000..fdc9a7e Binary files /dev/null and b/dingdan/migrations/__pycache__/0008_ptliushui_rename_cfliyou_tuikuanjilu_liyou_and_more.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0009_dingdan_liaotian_pindao.cpython-313.pyc b/dingdan/migrations/__pycache__/0009_dingdan_liaotian_pindao.cpython-313.pyc new file mode 100644 index 0000000..da6959a Binary files /dev/null and b/dingdan/migrations/__pycache__/0009_dingdan_liaotian_pindao.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0010_remove_dingdan_liaotian_pindao_dingdan_user1_id_and_more.cpython-313.pyc b/dingdan/migrations/__pycache__/0010_remove_dingdan_liaotian_pindao_dingdan_user1_id_and_more.cpython-313.pyc new file mode 100644 index 0000000..4343ecb Binary files /dev/null and b/dingdan/migrations/__pycache__/0010_remove_dingdan_liaotian_pindao_dingdan_user1_id_and_more.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0011_dingdanshangjia_sjnicheng.cpython-313.pyc b/dingdan/migrations/__pycache__/0011_dingdanshangjia_sjnicheng.cpython-313.pyc new file mode 100644 index 0000000..ecaf9c0 Binary files /dev/null and b/dingdan/migrations/__pycache__/0011_dingdanshangjia_sjnicheng.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0012_dingdan_auto_expire_at_dingdan_auto_task_id_and_more.cpython-313.pyc b/dingdan/migrations/__pycache__/0012_dingdan_auto_expire_at_dingdan_auto_task_id_and_more.cpython-313.pyc new file mode 100644 index 0000000..087b808 Binary files /dev/null and b/dingdan/migrations/__pycache__/0012_dingdan_auto_expire_at_dingdan_auto_task_id_and_more.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0013_alter_chufajilu_chuliid.cpython-313.pyc b/dingdan/migrations/__pycache__/0013_alter_chufajilu_chuliid.cpython-313.pyc new file mode 100644 index 0000000..5975a57 Binary files /dev/null and b/dingdan/migrations/__pycache__/0013_alter_chufajilu_chuliid.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0014_chufajilu_ssliyou_alter_chufajilu_sqzhuangtai_and_more.cpython-313.pyc b/dingdan/migrations/__pycache__/0014_chufajilu_ssliyou_alter_chufajilu_sqzhuangtai_and_more.cpython-313.pyc new file mode 100644 index 0000000..60a058e Binary files /dev/null and b/dingdan/migrations/__pycache__/0014_chufajilu_ssliyou_alter_chufajilu_sqzhuangtai_and_more.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0015_dingdan_tkly.cpython-313.pyc b/dingdan/migrations/__pycache__/0015_dingdan_tkly.cpython-313.pyc new file mode 100644 index 0000000..5915a87 Binary files /dev/null and b/dingdan/migrations/__pycache__/0015_dingdan_tkly.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0016_dingdan_is_cross_dingdan_partner_order_id.cpython-313.pyc b/dingdan/migrations/__pycache__/0016_dingdan_is_cross_dingdan_partner_order_id.cpython-313.pyc new file mode 100644 index 0000000..b65d3f1 Binary files /dev/null and b/dingdan/migrations/__pycache__/0016_dingdan_is_cross_dingdan_partner_order_id.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0017_dingdan_clkf_dingdan_dingdan_clkf_6793b1_idx.cpython-313.pyc b/dingdan/migrations/__pycache__/0017_dingdan_clkf_dingdan_dingdan_clkf_6793b1_idx.cpython-313.pyc new file mode 100644 index 0000000..b576337 Binary files /dev/null and b/dingdan/migrations/__pycache__/0017_dingdan_clkf_dingdan_dingdan_clkf_6793b1_idx.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0018_dingdanpingtai_jjjs_biaoshi.cpython-313.pyc b/dingdan/migrations/__pycache__/0018_dingdanpingtai_jjjs_biaoshi.cpython-313.pyc new file mode 100644 index 0000000..fd6ba33 Binary files /dev/null and b/dingdan/migrations/__pycache__/0018_dingdanpingtai_jjjs_biaoshi.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0019_dingdan_wechat_transaction_id_and_more.cpython-313.pyc b/dingdan/migrations/__pycache__/0019_dingdan_wechat_transaction_id_and_more.cpython-313.pyc new file mode 100644 index 0000000..713b6df Binary files /dev/null and b/dingdan/migrations/__pycache__/0019_dingdan_wechat_transaction_id_and_more.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0020_dingdan_dispatch_type_dingdan_partner_club_id.cpython-313.pyc b/dingdan/migrations/__pycache__/0020_dingdan_dispatch_type_dingdan_partner_club_id.cpython-313.pyc new file mode 100644 index 0000000..bb4406c Binary files /dev/null and b/dingdan/migrations/__pycache__/0020_dingdan_dispatch_type_dingdan_partner_club_id.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0021_presettlement_orderdashouhistory_and_more.cpython-313.pyc b/dingdan/migrations/__pycache__/0021_presettlement_orderdashouhistory_and_more.cpython-313.pyc new file mode 100644 index 0000000..b24821c Binary files /dev/null and b/dingdan/migrations/__pycache__/0021_presettlement_orderdashouhistory_and_more.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0022_alter_presettlement_dingdan_id_and_more.cpython-313.pyc b/dingdan/migrations/__pycache__/0022_alter_presettlement_dingdan_id_and_more.cpython-313.pyc new file mode 100644 index 0000000..8e3a1d4 Binary files /dev/null and b/dingdan/migrations/__pycache__/0022_alter_presettlement_dingdan_id_and_more.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/0023_alter_lilubiao_fadanpingtai.cpython-313.pyc b/dingdan/migrations/__pycache__/0023_alter_lilubiao_fadanpingtai.cpython-313.pyc new file mode 100644 index 0000000..b75a9a0 Binary files /dev/null and b/dingdan/migrations/__pycache__/0023_alter_lilubiao_fadanpingtai.cpython-313.pyc differ diff --git a/dingdan/migrations/__pycache__/__init__.cpython-313.pyc b/dingdan/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..66c98a9 Binary files /dev/null and b/dingdan/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/dingdan/models.py b/dingdan/models.py new file mode 100644 index 0000000..df26364 --- /dev/null +++ b/dingdan/models.py @@ -0,0 +1,589 @@ +from django.db import models + +class Dingdan(models.Model): + """订单主表""" + # 核心编号:唯一且不可为空[citation:3][citation:9] + dingdan_id = models.CharField(max_length=32, unique=True,db_index=True, verbose_name='订单ID') + # 状态与平台:用整数表示,具体含义由业务逻辑定义 + zhuangtai = models.IntegerField(default=1, null=True, blank=True,db_index=True, verbose_name='订单状态') + fadan_pingtai = models.IntegerField(null=True, blank=True, verbose_name='发单平台') + # 金额相关 + jine = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='订单金额') + dashou_fencheng = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='打手分成') + guanshi_fencheng = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, default=0, verbose_name='管事分成') + # 用户与商品关联ID + jiedan_dashou_id = models.CharField(max_length=32, null=True, db_index=True,blank=True, verbose_name='接单打手ID') + dashou_liuyan = models.TextField(null=True, blank=True, verbose_name='打手留言') + zhiding_id = models.CharField(max_length=32, null=True, blank=True, verbose_name='指定ID') + shangpin_id = models.IntegerField(null=True, blank=True, verbose_name='商品ID') + leixing_id = models.PositiveIntegerField(null=True, blank=True, verbose_name='商品类型ID') # 直接存数字 + tupian = models.CharField(max_length=500, null=True, blank=True, verbose_name='商品图片URL') + + yaoqiuleixing = models.PositiveIntegerField(null=True, blank=True, verbose_name='抢单要求类型') + huiyuan_id = models.CharField(max_length=6, null=True, blank=True, verbose_name='会员ID') + yongjin = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='佣金要求') + # 订单描述信息 + jieshao = models.TextField(null=True, blank=True, verbose_name='订单介绍') + beizhu = models.TextField(null=True, blank=True, verbose_name='订单备注') + tkly = models.CharField(max_length=500, blank=True, null=True, verbose_name='退款/拒绝理由') + # 新增字段:游戏昵称(前端提交的游戏昵称) + nicheng = models.CharField(max_length=50, null=True, blank=True, verbose_name='游戏昵称') + user1_id = models.CharField(max_length=30,null=True, db_index=True, verbose_name='用户1标识') + user2_id = models.CharField(max_length=30, null=True,db_index=True, verbose_name='用户2打手标识') + #liaotian_pindao = models.CharField(max_length=255, blank=True, null=True, verbose_name='聊天频道名') + # 时间戳 + + auto_task_id = models.CharField('延时任务ID', max_length=255, blank=True, default='') + auto_expire_at = models.DateTimeField('计划过期时间', null=True, blank=True) + pending_dispatch = models.BooleanField('待调度标记', default=False, db_index=True) + status_8_time = models.DateTimeField('状态变为8的时间', null=True, blank=True) + + + partner_order_id = models.CharField(max_length=32, null=True, blank=True, db_index=True,verbose_name='对方平台订单ID') + is_cross = models.IntegerField(default=0, verbose_name='是否跨平台订单(0否,1是)') + + dispatch_type = models.IntegerField(default=0, choices=[(1, '我方派单'), (2, '对方派单')], verbose_name='派发情况') + # 🔥 新增:对方俱乐部ID + partner_club_id = models.CharField(max_length=6, null=True, blank=True, db_index=True, verbose_name='对方俱乐部ID') + + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + clkf = models.CharField(max_length=20, null=True, db_index=True, verbose_name='处理客服') + wechat_transaction_id = models.CharField( + max_length=64, + blank=True, + null=True, + db_index=True, + verbose_name='微信支付订单号' + ) + + class Meta: + db_table = 'dingdan' + verbose_name = '订单' + verbose_name_plural = '订单' + # 为高频查询字段添加索引 + indexes = [ + models.Index(fields=['zhuangtai']), + models.Index(fields=['dingdan_id']), + models.Index(fields=['jiedan_dashou_id']), + models.Index(fields=['clkf']), + # 🆕 为新增字段添加索引(可选) + models.Index(fields=['wechat_transaction_id']), + + ] + + def __str__(self): + return self.dingdan_id + +class DingdanPingtai(models.Model): + """平台订单扩展表 (当 fadan_pingtai=1 时使用)""" + dingdan = models.OneToOneField(Dingdan, on_delete=models.CASCADE, related_name='pingtai_kuozhan', verbose_name='订单') + laoban_id = models.CharField(max_length=32, null=True, blank=True, db_index=True,verbose_name='老板ID') + laoban_pingjia = models.TextField(null=True, blank=True, verbose_name='老板评价') + jjjs_biaoshi = models.IntegerField(default=0, verbose_name='拒绝结算标记(1表示拒绝结算)') + + # ========== 新增字段 ========== + pingtai_shouyi = models.DecimalField(max_digits=10,decimal_places=2,null=True,blank=True,default=0.00,verbose_name='平台收益') + dianpu_shouyi = models.DecimalField(max_digits=10,decimal_places=2, null=True,blank=True,default=0.00,verbose_name='店铺收益') + dianpu_id = models.PositiveIntegerField(null=True,blank=True,db_index=True,verbose_name='所属店铺ID') + + # ---------- 上级分红快照 ---------- + shangji_dianpu_id = models.PositiveIntegerField(null=True, blank=True, db_index=True, + verbose_name='该订单的上级店铺ID') + shangji_fenhong = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, default=0.00, + verbose_name='上级分红金额') + + # ============================= + + + class Meta: + db_table = 'dingdan_pingtai' + verbose_name = '平台订单扩展' + verbose_name_plural = '平台订单扩展' + indexes = [ + models.Index(fields=['laoban_id']), + models.Index(fields=['dianpu_id']), # 为新字段加索引 + + + ] + + def __str__(self): + return f"平台扩展-{self.dingdan.dingdan_id}" + +class DingdanShangjia(models.Model): + """商家发单扩展表 (当 fadan_pingtai=2 时使用)""" + dingdan = models.OneToOneField(Dingdan, on_delete=models.CASCADE, related_name='shangjia_kuozhan', verbose_name='订单') + shangjia_id = models.CharField(max_length=32, null=True, blank=True,db_index=True, verbose_name='商家ID') + shangjia_pingjia = models.TextField(null=True, blank=True, verbose_name='商家评价') + cfliyou = models.TextField(null=True, blank=True, verbose_name='商家处罚理由') + sqzhuangtai =models.IntegerField(default=0, null=True, blank=True, verbose_name='处罚申请状态:0待处理,1成功,2驳回') + bhliyou = models.TextField(null=True, blank=True, verbose_name='驳回商家处罚理由') + sjnicheng =models.CharField(max_length=50,null=True, verbose_name='商家昵称') + partner_yonghu_id = models.CharField(max_length=32, blank=True, null=True, verbose_name='对方平台用户ID') + + # 在 dingdan/models.py 的 DingdanShangjia 类中添加 + shangyige_dingdan_id = models.CharField( + max_length=32, + null=True, + blank=True, + verbose_name='上一个订单ID' + ) + + class Meta: + db_table = 'dingdan_shangjia' + verbose_name = '商家订单扩展' + verbose_name_plural = '商家订单扩展' + indexes = [ + models.Index(fields=['shangjia_id']), + + ] + + def __str__(self): + return f"商家扩展-{self.dingdan.dingdan_id}" + + + + + + + + + + + + +class DingdanPeihu(models.Model): + """陪护订单表(一个订单最多2个陪护)""" + # 关联订单(一对一,陪护订单依附于原订单) + dingdan_id = models.CharField(max_length=32, unique=True, db_index=True, verbose_name='订单ID') + # 主打手ID(冗余,方便查询,必须与订单的 jiedan_dashou_id 一致) + zhudashou_id = models.CharField(max_length=32, db_index=True, verbose_name='主打手ID') + # 实际接单陪护 + peihu1_id = models.CharField(max_length=32, null=True, blank=True, db_index=True, verbose_name='陪护1打手ID') + peihu2_id = models.CharField(max_length=32, null=True, blank=True, db_index=True, verbose_name='陪护2打手ID') + # 订单状态(1=待抢单,2=已抢待确认,3=进行中,4=已完成,5=已取消,7=指定状态...) + zhuangtai = models.IntegerField(default=1, verbose_name='陪护订单状态') + # 需求配置 + peihu_xuqiu_shuliang = models.IntegerField(default=1, verbose_name='陪护需求数量') + yongjin_yaoqiu = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='抢单佣金要求') + # 等级需求(关联称号ID,可空表示无限制) + peihu1_dengji_xuqiu = models.PositiveIntegerField(null=True, blank=True, verbose_name='陪护1等级需求(称号ID)') + peihu2_dengji_xuqiu = models.PositiveIntegerField(null=True, blank=True, verbose_name='陪护2等级需求(称号ID)') + # 处罚与分红比例(格式:"1/5" 或 "2/5") + peihu1_zhadan_danze = models.CharField(max_length=10, null=True, blank=True, verbose_name='陪护1炸单担责比例') + peihu2_zhadan_danze = models.CharField(max_length=10, null=True, blank=True, verbose_name='陪护2炸单担责比例') + peihu1_fenhong = models.CharField(max_length=10, null=True, blank=True, verbose_name='陪护1分红比例') + peihu2_fenhong = models.CharField(max_length=10, null=True, blank=True, verbose_name='陪护2分红比例') + # 指定陪护(可预设打手ID,可为空) + zhiding_peihu1 = models.CharField(max_length=32, null=True, blank=True, verbose_name='指定陪护1 ID') + zhiding_peihu2 = models.CharField(max_length=32, null=True, blank=True, verbose_name='指定陪护2 ID') + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'dingdan_peihu' + verbose_name = '陪护订单' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['zhudashou_id']), + models.Index(fields=['peihu1_id']), + models.Index(fields=['peihu2_id']), + models.Index(fields=['zhuangtai']), + ] + + + + + +class CrossPlatformOrderData(models.Model): + """ + 跨平台订单扩展数据表 + 存储我方订单与对方平台订单的关联及对方订单状态 + """ + dingdan_id = models.CharField(max_length=32, unique=True, db_index=True, verbose_name='我方订单ID') + partner_order_id = models.CharField(max_length=32, db_index=True, verbose_name='对方平台订单ID') + partner_club_id = models.CharField(max_length=6, db_index=True, verbose_name='对方俱乐部ID') + partner_order_status = models.IntegerField(default=0, verbose_name='对方平台订单状态') # 对方订单当前状态码 + partner_dashou_id = models.CharField(max_length=32, blank=True, default='', verbose_name='对方平台接单打手ID') + partner_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='对方平台订单金额') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'cross_platform_order_data' + verbose_name = '跨平台订单扩展' + verbose_name_plural = '跨平台订单扩展' + indexes = [ + models.Index(fields=['dingdan_id']), + models.Index(fields=['partner_order_id']), + models.Index(fields=['partner_club_id']), + ] + + def __str__(self): + return f"{self.dingdan_id} -> {self.partner_order_id}" + + + +# 建议路径:dingdan/models.py 或 独立 fadan/models.py +from django.db import models + +class Fadan(models.Model): + """罚单表——平台各类处罚信息""" + id = models.AutoField(primary_key=True, verbose_name='自增主键') + beichufa_id = models.CharField(max_length=20, db_index=True, verbose_name='被处罚用户ID') # 直接用yonghuid,后续可改为外键 + shenqing_chufa = models.CharField(max_length=20, blank=True, null=True, verbose_name='申请处罚者ID') + shenfen = models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '商家'), (4, '组长')], default=1, verbose_name='被处罚者身份') + chufaliyou = models.TextField(blank=True, null=True, verbose_name='处罚理由') + fakuanjine = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='罚款金额') + guanliandingdan_id = models.CharField(max_length=32, blank=True, null=True, db_index=True, verbose_name='关联订单ID') + zhuangtai = models.IntegerField(choices=[(1, '待缴纳'), (2, '已缴纳'), (3, '申诉中'), (4, '已驳回')], default=1, db_index=True, verbose_name='罚单状态') + yingxiang_qiangdan = models.IntegerField(choices=[(1, '影响抢单'), (0, '不影响抢单')], default=1, db_index=True, verbose_name='是否影响抢单') + shensuliyou = models.TextField(blank=True, null=True, verbose_name='申诉理由') + tongyi_chufaliyou = models.TextField(blank=True, null=True, verbose_name='同意处罚理由') + bohuiliyou = models.TextField(blank=True, null=True, verbose_name='驳回理由') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + # ========== 新增字段 ========== + shenqingren_fenhong_jine = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, + verbose_name='申请人分红金额' + ) + # 在 Fadan 模型中添加 + partner_club_id = models.CharField(max_length=6, blank=True, null=True, db_index=True, verbose_name='对方俱乐部ID') + # 在 class Fadan 中添加 + shenqingren_shenfen = models.IntegerField( + choices=[(1, '客服'), (2, '售后'), (3, '管理员'),(4,'店铺'),(5,'商家')], # 按实际扩展 + default=1, + verbose_name='申请处罚人身份' + ) + chulizhe_shenfen = models.IntegerField( + choices=[(1, '客服'), (2, '售后'), (3, '管理员')], + default=2, + verbose_name='处理者身份' + ) + chulizhe = models.CharField(max_length=20, blank=True, null=True, verbose_name='处理者') + + class Meta: + db_table = 'fadan' + verbose_name = '罚单' + verbose_name_plural = '罚单' + indexes = [ + models.Index(fields=['beichufa_id', 'yingxiang_qiangdan', 'zhuangtai'], name='fadan_qiangdan_check'), + models.Index(fields=['guanliandingdan_id'], name='fadan_dingdan_idx'), + ] + + def __str__(self): + return f"{self.beichufa_id} - 罚款{self.fakuanjine}元 - 状态{self.zhuangtai}" + + +# 与 Fadan 放在同一个文件或独立的 fadan/models.py +class FadanFenhong(models.Model): + """罚款分红表——记录罚款产生的分红信息""" + id = models.AutoField(primary_key=True, verbose_name='自增主键') + fenhongzhe_id = models.CharField(max_length=20, db_index=True, verbose_name='分红者ID') + zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='分红总额') + ketixian_fenhong = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='可提现分红') + shenfen = models.IntegerField(choices=[(1, '商家'), (2, '客服'), (3, '平台管理者'),(4,'店铺'),(5, '售后')], default=1, verbose_name='分红者身份') + fenhong_cishu = models.IntegerField(default=0, verbose_name='分红次数') + # ✅ 新增字段:是否单独开启分红利率(若开启,则使用本记录的 yingde_lilv;否则使用全局配置) + is_individual_rate = models.BooleanField(default=False, verbose_name='是否单独开启分红利率') + yingde_lilv = models.DecimalField(max_digits=5, decimal_places=4, default=0.0000, verbose_name='分红应得利率') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'fadan_fenhong' + verbose_name = '罚款分红' + verbose_name_plural = '罚款分红' + indexes = [ + models.Index(fields=['fenhongzhe_id'], name='fadan_fenhong_zhe_idx'), + models.Index(fields=['shenfen'], name='fadan_fenhong_sf_idx'), + ] + + def __str__(self): + return f"{self.fenhongzhe_id} - 分红{self.ketixian_fenhong}元" + + +class FadanFenhongLilv(models.Model): + shenfen = models.IntegerField(primary_key=True, choices=[(1, '客服'), (2, '售后'), (3, '管理员'), (4, '店铺'), (5, '商家')], verbose_name='申请处罚人身份') + lilv = models.DecimalField(max_digits=5, decimal_places=4, default=0.0000, verbose_name='分红费率') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'fadan_fenhong_lilv' + verbose_name = '罚款分红费率配置' + verbose_name_plural = verbose_name + + def __str__(self): + return f"身份{self.shenfen} - 费率{self.lilv}" + + +# ==================== 罚款申诉图片表 ==================== +class FadanShensuTupian(models.Model): + """罚款申诉图片,关联罚单""" + fadan = models.ForeignKey(Fadan, on_delete=models.CASCADE, related_name='shensu_tupians', verbose_name='关联罚单') + beichufa_id = models.CharField(max_length=7, db_index=True, verbose_name='被处罚用户ID') # 冗余,便于直接按用户查 + tupian_url = models.CharField(max_length=500, verbose_name='图片相对URL') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + # 在 FadanShensuTupian 模型中添加 + yongtu = models.IntegerField( + choices=[(1, '处罚证据图片'), (2, '申诉图片')], + default=2, + verbose_name='图片用途' + ) + + class Meta: + db_table = 'fadan_shensu_tupian' + verbose_name = '罚款申诉图片' + verbose_name_plural = '罚款申诉图片' + indexes = [ + models.Index(fields=['fadan_id']), + models.Index(fields=['beichufa_id']), + ] + + + +# apps/dingdan/models.py 中添加 + +class PreSettlement(models.Model): + """ + 预结算表 + 用于记录对方平台派单订单的待结算款项,等待对方确认后才能真正入账 + """ + STATUS_CHOICES = ( + (0, '待结算'), + (1, '已结算'), + (2, '已拒绝'), + ) + # 我方订单ID + dingdan_id = models.CharField(max_length=32, db_index=True, verbose_name='我方订单ID') + # 对方订单ID + partner_order_id = models.CharField(max_length=32, db_index=True, verbose_name='对方订单ID') + # 对方俱乐部ID + partner_club_id = models.CharField(max_length=6, db_index=True, verbose_name='对方俱乐部ID') + # 接单打手ID + dashou_id = models.CharField(max_length=7, db_index=True, verbose_name='接单打手ID') + # 打手应分金额 + amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='打手应分金额') + # 订单金额(原金额,可选) + order_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='订单原金额') + # 结算状态 + status = models.SmallIntegerField(choices=STATUS_CHOICES, default=0, verbose_name='结算状态') + # 处理时间(结算或拒绝的时间) + process_time = models.DateTimeField(null=True, blank=True, verbose_name='处理时间') + # 处理客服/对方操作人(可选) + operator = models.CharField(max_length=32, blank=True, default='', verbose_name='操作人') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'pre_settlement' + verbose_name = '预结算表' + verbose_name_plural = verbose_name + # 添加这个联合唯一约束 + unique_together = [['dingdan_id', 'dashou_id']] + indexes = [ + models.Index(fields=['dingdan_id']), + models.Index(fields=['partner_order_id']), + models.Index(fields=['partner_club_id']), + models.Index(fields=['dashou_id']), + models.Index(fields=['status']), + ] + + def __str__(self): + return f"预结算-{self.dingdan_id} -> {self.partner_order_id}" + + +# apps/dingdan/models.py 中添加 + +class OrderDashouHistory(models.Model): + """ + 订单打手变更历史表 + 记录订单每次更换打手的操作 + """ + dingdan_id = models.CharField(max_length=32, db_index=True, verbose_name='订单ID') + dashou_id = models.CharField(max_length=7, db_index=True, verbose_name='打手ID') + times = models.PositiveSmallIntegerField(verbose_name='第几次接单(从1开始)') + operator = models.CharField(max_length=11, verbose_name='操作客服ID') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'order_dashou_history' + verbose_name = '订单打手变更历史' + verbose_name_plural = '订单打手变更历史' + indexes = [ + models.Index(fields=['dingdan_id']), + models.Index(fields=['dashou_id']), + ] + + def __str__(self): + return f"{self.dingdan_id} -> {self.dashou_id} 第{self.times}次" + + +class Dashoutupian(models.Model): + dingdan_id = models.CharField(max_length=32,db_index=True, null=True, verbose_name='订单ID') + dashou = models.CharField(max_length=32, null=True, blank=True,db_index=True, verbose_name='接单打手ID') + tupian = models.CharField(max_length=500, null=True, blank=True, verbose_name='打手提交图片URL') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + class Meta: + db_table = 'dashoutupian' + + indexes = [ + models.Index(fields=['dashou']), + models.Index(fields=['dingdan_id']), + + ] + + + +class Pingfen(models.Model): + dashou = models.CharField(max_length=32, null=True, blank=True, db_index=True,verbose_name='接单打手ID') + zongfen = models.PositiveIntegerField(default=0, verbose_name='总分') + cishu = models.PositiveIntegerField(default=0, verbose_name='评分次数') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'pingfenbiao' + verbose_name = '评分表' + indexes = [ + models.Index(fields=['dashou']), + ] + def __str__(self): + return f"{self.dashou} - 平均分: {self.pingjun_fen():.2f}" + + def pingjun_fen(self): + """计算平均分""" + if self.cishu > 0: + return float(self.zongfen) / self.cishu + return 0.0 + + + +class Lilubiao(models.Model): + lilu = models.DecimalField(max_digits=5, decimal_places=4,null=True, verbose_name='平台分成利率,1是平台,3是商家派单抽成,5是打手提现抽,6管事提现抽,8.组长提现抽9.审核官提现抽,10.商家提现抽,11打手保证金提现抽,12.押金分红,13.管事分红') + #sahngjialilu = models.DecimalField(max_digits=5, decimal_places=4,null=True, verbose_name='商家利率') + fadanpingtai = models.CharField(max_length=10,null=True,verbose_name='利率类型,平台分成利率,1是平台订单打手分成,3是商家订单打手分成,5:打手提现佣金抽成6:管事提现抽成,8:组长提现抽成') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + class Meta: + db_table = 'lilubiao' + + + + +class Chufajilu(models.Model): + dashouid = models.CharField(max_length=7, null=True, db_index=True, verbose_name='被罚打手ID') + qingqiuid = models.CharField(max_length=7, null=True, db_index=True, verbose_name='请求处罚ID') + chuliid = models.CharField(max_length=11, null=True, db_index=True, verbose_name='处理处罚ID') + cfliyou = models.TextField(null=True, blank=True, verbose_name='处罚理由') + sqzhuangtai = models.IntegerField(default=0, null=True, blank=True, verbose_name='处罚申请状态:0待处理,1成功,2驳回,3申诉') + bhliyou = models.TextField(null=True, blank=True, verbose_name='驳回处罚理由') + ssliyou = models.TextField(null=True, blank=True, verbose_name='打手申诉理由') + jifen = models.IntegerField(default=0, verbose_name='扣除打手积分') + dingdan_id = models.CharField(max_length=32,null=True,db_index=True, verbose_name='订单ID') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + class Meta: + db_table = 'chufajilu' + indexes = [ + models.Index(fields=['dashouid']), + models.Index(fields=['qingqiuid']), + models.Index(fields=['chuliid']), + models.Index(fields=['dingdan_id']), + ] + + +class Chufatupian(models.Model): + dingdan_id = models.CharField(max_length=32,db_index=True, null=True, verbose_name='订单ID') + yonghuid = models.CharField(max_length=32, null=True, blank=True,db_index=True, verbose_name='用户ID') + tupian = models.CharField(max_length=500, null=True, blank=True, verbose_name='证据图片URL') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + class Meta: + db_table = 'chufatupian' + + indexes = [ + models.Index(fields=['yonghuid']), + models.Index(fields=['dingdan_id']), + + ] + + +class Tuikuanjilu(models.Model): + dingdan_id = models.CharField(max_length=32, unique=True, verbose_name='订单ID') + dashouid = models.CharField(max_length=7, null=True, verbose_name='接单打手ID') + qingqiuid = models.CharField(max_length=7, null=True, verbose_name='请求用户ID') + chuliid = models.CharField(max_length=7, null=True, db_index=True, verbose_name='处理人ID') + liyou = models.TextField(null=True, blank=True, verbose_name='理由') + sqzhuangtai = models.IntegerField(default=0, null=True, blank=True,db_index=True, verbose_name='申请状态:0待处理,1成功,2驳回') + bhliyou = models.TextField(null=True, blank=True, verbose_name='驳回理由') + jine = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='订单金额') + dashou_fencheng = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='打手分成') + jieshao = models.TextField(null=True, blank=True, verbose_name='订单介绍') + beizhu = models.TextField(null=True, blank=True, verbose_name='订单备注') + # 新增字段:游戏昵称(前端提交的游戏昵称) + nicheng = models.CharField(max_length=50, null=True, blank=True, verbose_name='游戏昵称') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + class Meta: + db_table = 'tuikuanjilu' + indexes = [ + models.Index(fields=['chuliid']), + models.Index(fields=['sqzhuangtai']), + ] + + +class Ptliushui(models.Model): + zonge = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='收取总金额') + zhichuzonge = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='支出总金额') + zongshouyi = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='总收益') + rizonge = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='日收取') + rizhichu = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='日支出') + rishouyi = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='日收益') + zongshoucishu = models.IntegerField(default=0, verbose_name='总收取次数') + rishoucishu = models.IntegerField(default=0, verbose_name='日收取次数') + zongzhichucishu = models.IntegerField(default=0, verbose_name='总支出次数') + rizhichucishu = models.IntegerField(default=0, verbose_name='日支出次数') + class Meta: + db_table = 'ptliushui' + + +# models.py - ChatRelation 模型 + + + +class Liaotian(models.Model): + """聊天关系表 - 极简设计,不设置唯一约束""" + # 主键 + id = models.BigAutoField(primary_key=True, verbose_name='主键ID') + + # 聊天双方的用户标识(GoEasy的user_id) + user1_id = models.CharField(max_length=30, db_index=True, verbose_name='用户1标识') + user2_id = models.CharField(max_length=30, db_index=True, verbose_name='用户2标识') + + # 关联的订单(可以为空,但实际业务中应该关联) + dingdan_id = models.CharField(max_length=32, unique=True, verbose_name='订单ID') + # 状态与平台:用整数表示,具体含义由业务逻辑定义 + zhuangtai = models.IntegerField(default=1,db_index=True,choices=[(1, '开启中'), (0, '已关闭')],verbose_name='聊天状态') + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'liaotian' + indexes = [ + models.Index(fields=['user1_id']), + models.Index(fields=['user2_id']), + models.Index(fields=['zhuangtai']), + ] + + def __str__(self): + return f'{self.user1_id} ↔ {self.user2_id} ({self.zhuangtai})' + + + diff --git a/dingdan/signals.py b/dingdan/signals.py new file mode 100644 index 0000000..c14aa5f --- /dev/null +++ b/dingdan/signals.py @@ -0,0 +1,272 @@ +""" +阿龙电竞 - 订单信号处理(优化版) +核心原则:绝对不影响订单提交的性能和稳定性 +设计理念:订单状态变化只记录,不执行,由接口或定时任务处理具体逻辑 +""" + +from django.db.models.signals import pre_save +from django.dispatch import receiver +from django.conf import settings +from django.utils import timezone +import logging +import threading +import time + +from dingdan.models import Dingdan + +# dingdan/signals.py +""" +阿龙电竞 - 订单信号处理器(生产稳定版) +经过验证,100%可靠 +""" + +import sys +import importlib +import logging +from django.db.models.signals import pre_save +from django.dispatch import receiver +from django.conf import settings +from django.utils import timezone + +logger = logging.getLogger(__name__) + + +# 🔧 关键修复:确保redis模块正确加载 +def ensure_redis_loaded(): + """确保redis模块正确加载,避免导入冲突""" + if 'redis' not in sys.modules: + # 如果redis模块未加载,尝试导入 + try: + import redis + #logger.debug("✅ Redis模块正常加载") + except ImportError: + #logger.warning("⚠️ Redis模块未安装,尝试自动安装") + import subprocess + subprocess.call([sys.executable, "-m", "pip", "install", "redis==4.5.4", "-q"]) + import redis + + # 确保redis.Redis属性存在 + import redis + if redis.Redis is None: + #logger.warning("⚠️ Redis.Redis属性为None,强制重新加载模块") + importlib.reload(redis) + + return redis + + +# 预加载redis模块 +ensure_redis_loaded() + + +@receiver(pre_save, sender='dingdan.Dingdan') +def handle_order_status_8(sender, instance, **kwargs): + """ + 订单状态变为8时,提交定时任务 + """ + # 跳过新增的订单 + if instance.pk is None: + return + + try: + # 动态导入,避免循环依赖 + from dingdan.models import Dingdan + + # 获取旧状态 + old = Dingdan.objects.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first() + + # 状态从 非8 变为 8 + if old != 8 and instance.zhuangtai == 8: + logger.info(f"📅 订单 {instance.dingdan_id} 状态变为8,提交定时任务") + + # 设置时间标记 + instance.status_8_time = timezone.now() + instance.pending_dispatch = True + + # 使用配置中的超时时间 + expire_seconds = getattr(settings, 'ORDER_EXPIRE_SECONDS', 48 * 60 * 60) + + # 提交Celery任务 + try: + from dingdan.tasks import process_expired_order + + task_result = process_expired_order.apply_async( + args=[instance.dingdan_id], + countdown=expire_seconds, + queue='order_tasks', + priority=9 + ) + + logger.info(f"✅ 任务提交成功,订单: {instance.dingdan_id}, 任务ID: {task_result.id}") + + # 保存任务信息 + instance.auto_task_id = task_result.id + instance.auto_expire_at = timezone.now() + timezone.timedelta(seconds=expire_seconds) + + except Exception as e: + logger.error(f"❌ 任务提交失败: {e}") + instance.pending_dispatch = True + instance.auto_task_id = f"failed_{timezone.now().timestamp()}" + + # 状态从 8 变为 非8 + elif old == 8 and instance.zhuangtai != 8: + instance.status_8_time = None + instance.pending_dispatch = False + instance.auto_task_id = '' + logger.info(f"📅 订单 {instance.dingdan_id} 状态离开8") + + except Exception as e: + logger.error(f"信号处理失败: {e}") + + +logger = logging.getLogger(__name__) + +'''def try_start_celery_task_async(dingdan_id, countdown): + """ + 异步尝试启动Celery任务(在独立线程中运行) + 如果Redis不可用,立即失败,不影响主流程 + """ + def _async_task(): + try: + # 设置超时时间(500毫秒),避免长时间等待 + import socket + socket.setdefaulttimeout(0.5) + + from dingdan.tasks import process_expired_order + from celery.exceptions import TimeoutError + + # 尝试启动任务,设置极短的超时 + task = process_expired_order.apply_async( + args=[dingdan_id], + countdown=countdown, + queue='order_tasks', + priority=9, + expires=countdown + 3600 # 任务1小时后过期,避免堆积 + ) + + # 不等待任务结果,立即返回 + logger.info(f"订单 {dingdan_id} 异步任务已提交(任务ID: {task.id})") + + except Exception as e: + # 记录警告,但不要影响主流程 + logger.warning(f"订单 {dingdan_id} 异步任务提交失败: {str(e)}") + # 这个失败不影响订单保存,系统有补偿机制 + + # 启动异步线程(后台运行,不阻塞主流程) + thread = threading.Thread(target=_async_task) + thread.daemon = True # 设置为守护线程,主线程退出时会自动结束 + thread.start() + +@receiver(pre_save, sender=Dingdan) +def handle_order_status_change_lightweight(sender, instance, **kwargs): + """ + 轻量级订单状态变化监听器 + 设计原则: + 1. 绝对不阻塞订单保存 + 2. 不依赖外部服务可用性 + 3. 只记录,不执行 + """ + try: + # 1. 快速检查:如果是新建订单,直接返回 + if instance.pk is None: + return + + # 2. 快速获取旧状态(使用数据库当前值,不加载完整对象) + try: + old_status = Dingdan.objects.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first() + except Exception: + old_status = None + + # 3. 状态变化判断 + if old_status is None or old_status != instance.zhuangtai: + # 状态发生变化,记录到订单扩展字段或日志(但不要阻塞) + try: + # 这里只是示例,实际可以根据需要记录状态变化历史 + pass + except: + pass # 即使记录失败也不影响主流程 + + # 4. 特殊状态处理:状态变为8 + if old_status != 8 and instance.zhuangtai == 8: + # 4.1 立即记录日志(不等待) + logger.info(f"订单 {instance.dingdan_id} 状态变为8") + + # 4.2 异步尝试启动定时任务(不阻塞主流程) + # 使用线程池或直接新线程,确保不阻塞 + try: + # 启动异步线程尝试提交Celery任务 + if hasattr(settings, 'CELERY_BROKER_URL') and settings.CELERY_BROKER_URL: + # 在独立线程中尝试,完全不阻塞 + threading.Thread( + target=try_start_celery_task_async, + args=(instance.dingdan_id, settings.ORDER_EXPIRE_SECONDS), + daemon=True + ).start() + except Exception as e: + # 即使线程启动失败也不影响主流程 + logger.warning(f"启动异步任务线程失败: {str(e)}") + + # 5. 状态从8变为其他 + elif old_status == 8 and instance.zhuangtai != 8: + # 状态不再是8,记录日志即可 + # 定时任务执行时会检查状态,如果不是8会自动跳过 + logger.info(f"订单 {instance.dingdan_id} 状态从8变为{instance.zhuangtai}") + + except Exception as e: + # 捕获所有异常,确保不影响订单保存 + # 只记录错误,不抛出异常 + logger.error(f"信号处理异常(已捕获,不影响订单): {str(e)}")''' + + +'''from django.db.models.signals import pre_save +from django.dispatch import receiver +from django.conf import settings +import logging + +from dingdan.models import Dingdan +from dingdan.tasks import process_expired_order + +logger = logging.getLogger(__name__) + +@receiver(pre_save, sender=Dingdan) +def handle_order_status_change(sender, instance, **kwargs): + """ + 监听订单状态变化 + 当状态变为8时:启动48小时延时任务 + 当状态从8变为其他时:任务执行时会自动跳过,无需取消 + """ + try: + # 如果是新建订单,没有旧记录,直接返回 + if instance.pk is None: + return + + # 获取数据库中旧的订单记录 + old_instance = Dingdan.objects.get(pk=instance.pk) + + # ============ 状态从非8变为8 ============ + if old_instance.zhuangtai != 8 and instance.zhuangtai == 8: + # 启动48小时后的延时任务(countdown单位:秒) + task = process_expired_order.apply_async( + args=[instance.dingdan_id], + countdown=settings.ORDER_EXPIRE_SECONDS, # 172800秒 = 48小时 + queue='order_tasks', + priority=9 # 高优先级 + ) + + logger.info( + f"订单 {instance.dingdan_id} 状态变为8,已启动48小时延时任务," + f"任务ID: {task.id}" + ) + + # 记录任务启动成功(可选,可以存储到订单表或日志表) + + # ============ 状态从8变为其他 ============ + # 我们不需要取消任务,因为任务执行时会检查状态是否为8 + # 如果不是状态8,任务会自动跳过处理 + + except Dingdan.DoesNotExist: + # 新订单,无需处理 + pass + except Exception as e: + logger.error(f"处理订单状态变化信号失败,订单ID: {instance.dingdan_id if hasattr(instance, 'dingdan_id') else '未知'}, 错误: {str(e)}") + # 不抛出异常,避免影响订单保存的主流程 + # 信号处理失败不影响订单保存,有周期性检查作为备用方案''' \ No newline at end of file diff --git a/dingdan/tasks.py b/dingdan/tasks.py new file mode 100644 index 0000000..b284f78 --- /dev/null +++ b/dingdan/tasks.py @@ -0,0 +1,198 @@ +""" +阿龙电竞 - 订单定时任务 +处理订单超时自动确认等逻辑 +完整版本,包含所有必要的异常处理 +""" + +from celery import shared_task +from django.db import transaction +from django.db.models import F, Q +from django.conf import settings +from django.utils import timezone +from datetime import timedelta +from decimal import Decimal +import logging + +# 导入模型 - 确保路径正确 +from dingdan.models import Dingdan, DingdanShangjia +from yonghu.models import UserMain, UserDashou, UserShangjia +from utils.celery_utils import safe_decimal_operation, log_task_execution, rollback_on_failure + +logger = logging.getLogger(__name__) + +@shared_task(bind=True, max_retries=3, default_retry_delay=60) +@rollback_on_failure("处理超时订单") +def process_expired_order(self, dingdan_id): + """ + 处理单个超时订单(订单状态=8,超过48小时未处理) + 这个任务在订单状态变为8的48小时后执行 + :param dingdan_id: 订单ID + :return: 处理结果 + """ + try: + # 使用select_for_update锁定记录,防止并发修改 + with transaction.atomic(): + # 获取订单,同时锁定记录 + order = Dingdan.objects.select_for_update().get( + dingdan_id=dingdan_id + ) + + # 再次检查订单状态是否为8 + if order.zhuangtai != 8: + logger.info(f"订单{dingdan_id}状态已不是8(当前:{order.zhuangtai}),跳过处理") + return f"订单{dingdan_id}状态已变更,跳过处理" + + # 获取发单平台类型 + fadan_pingtai = order.fadan_pingtai + + # 获取打手ID和分成金额 + dashou_id = order.jiedan_dashou_id + dashou_fencheng = safe_decimal_operation(order.dashou_fencheng) + + # 1. 更新订单状态为3(已完成) + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + + # 2. 更新打手信息(如果存在) + updated_dashou = False + if dashou_id: + try: + # 查询打手用户主表 + dashou_user = UserMain.objects.filter(yonghuid=dashou_id).first() + if dashou_user: + # 获取打手扩展表 + dashou_profile = getattr(dashou_user, 'dashou_profile', None) + if dashou_profile: + # 使用F表达式原子更新 + UserDashou.objects.filter(pk=dashou_profile.pk).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 + ) + updated_dashou = True + logger.info(f"更新打手{dashou_id}信息成功") + except Exception as e: + logger.error(f"更新打手信息失败,打手ID: {dashou_id}, 错误: {str(e)}") + # 继续执行,不中断流程 + + # 3. 如果是商家发单,更新商家信息 + updated_shangjia = False + if fadan_pingtai == 2: # 商家发单 + try: + # 获取商家扩展信息 + shangjia_kuozhan = getattr(order, 'shangjia_kuozhan', None) + if shangjia_kuozhan: + shangjia_id = shangjia_kuozhan.shangjia_id + if shangjia_id: + # 查询商家用户 + shangjia_user = UserMain.objects.filter(yonghuid=shangjia_id).first() + if shangjia_user: + shangjia_profile = getattr(shangjia_user, 'shop_profile', None) + if shangjia_profile: + # 更新商家成交订单数量 + UserShangjia.objects.filter(pk=shangjia_profile.pk).update( + chengjiao=F('chengjiao') + 1 + ) + updated_shangjia = True + logger.info(f"更新商家{shangjia_id}信息成功") + except Exception as e: + logger.error(f"更新商家信息失败,订单ID: {dingdan_id}, 错误: {str(e)}") + # 继续执行,不中断流程 + + # 记录任务执行结果 + result_msg = f"订单{dingdan_id}自动结算完成" + if updated_dashou: + result_msg += ",打手信息已更新" + if updated_shangjia: + result_msg += ",商家信息已更新" + # 12. 更新平台收益流水(仅在平台发单时) + if 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)}") + + log_task_execution(f"自动结算订单 {dingdan_id}", True, result_msg) + return result_msg + + except Dingdan.DoesNotExist: + error_msg = f"订单{dingdan_id}不存在" + logger.warning(error_msg) + return error_msg + except Exception as e: + error_msg = f"处理订单{dingdan_id}时发生错误: {str(e)}" + logger.error(error_msg) + + # 重试机制 + if self.request.retries < self.max_retries: + logger.info(f"订单{dingdan_id}处理失败,准备第{self.request.retries + 1}次重试") + raise self.retry(exc=e) + + log_task_execution(f"自动结算订单 {dingdan_id}", False, error_msg) + return error_msg + +@shared_task +@rollback_on_failure("批量检查超时订单") +def check_order_expire_task(): + """ + 批量检查超时订单(补偿机制) + 每5分钟执行一次,作为信号处理的备用方案 + 查找状态为8且创建时间超过72小时的订单(宽松时间,防止误处理) + """ + try: + # 计算更宽松的时间点:当前时间减去72小时(48+24小时) + # 这样避免误处理刚变为状态8的订单 + check_time = timezone.now() - timedelta(seconds=settings.ORDER_EXPIRE_SECONDS + 86400) # 48+24=72小时 + + # 查询状态为8且创建时间早于检查时间的订单 + expired_orders = Dingdan.objects.filter( + zhuangtai=8, + create_time__lt=check_time + ).values_list('dingdan_id', flat=True) + + expired_count = expired_orders.count() + + if expired_count == 0: + logger.info("补偿检查:无超时订单") + return {"success": True, "message": "无超时订单", "count": 0} + + # 分批处理超时订单(避免一次处理太多) + batch_size = 10 + processed_count = 0 + + for i in range(0, expired_count, batch_size): + batch = expired_orders[i:i+batch_size] + + # 异步处理每个超时订单 + for dingdan_id in batch: + process_expired_order.apply_async( + args=[dingdan_id], + queue='order_tasks', + priority=8 # 补偿任务的优先级低一些 + ) + processed_count += 1 + + result_msg = f"补偿检查:发现{expired_count}个可能超时的订单,已提交处理{processed_count}个" + log_task_execution("批量检查超时订单", True, result_msg) + + return { + "success": True, + "message": result_msg, + "total_count": expired_count, + "processed_count": processed_count + } + + except Exception as e: + error_msg = f"批量检查超时订单失败: {str(e)}" + logger.error(error_msg) + log_task_execution("批量检查超时订单", False, error_msg) + + return { + "success": False, + "message": error_msg, + "error": str(e) + } \ No newline at end of file diff --git a/dingdan/tests.py b/dingdan/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/dingdan/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/dingdan/tongzhi_tasks.py b/dingdan/tongzhi_tasks.py new file mode 100644 index 0000000..874c4b7 --- /dev/null +++ b/dingdan/tongzhi_tasks.py @@ -0,0 +1,71 @@ +# dingdan/tongzhi_tasks.py +import logging +from concurrent.futures import ThreadPoolExecutor, as_completed +from a_long_dianjing.celery_app import app +from yonghu.models import OfficialAccountUser +from utils.weixin_broadcast import WeixinBroadcastSender + +logger = logging.getLogger('weixin_broadcast') + + +@app.task(bind=True, max_retries=3, default_retry_delay=60) +def dingdan_guangbo(self, order_info): + """ + 异步并发广播订单模板消息给所有关注服务号的用户 + order_info 字典字段示例: + { + 'dingdan_id': 'PT123456', + 'game_type': '王者荣耀', + 'amount': '88.00', + 'order_desc': '星耀上王者' + } + """ + sender = WeixinBroadcastSender() + template_data = sender.build_order_message_data(order_info) + order_id = order_info.get('dingdan_id') + + # 查询所有已关注用户 + try: + openids = list( + OfficialAccountUser.objects + .filter(is_subscribed=True) + .values_list('official_openid', flat=True) + ) + except Exception as e: + logger.error(f'查询订阅用户失败: {e}') + raise self.retry(exc=e) + + if not openids: + return {'success': True, 'msg': '无订阅用户'} + + total = len(openids) + logger.info(f'📊 Celery 广播订单 {order_id} 开始,目标人数: {total}') + + success = 0 + fail = 0 + + # 20 个线程并发发送,微信接口耗时 200-500ms,1000 人约 15 秒完成 + with ThreadPoolExecutor(max_workers=20) as executor: + future_map = { + executor.submit(sender.send_to_user, openid, template_data, order_id): openid + for openid in openids + } + for future in as_completed(future_map): + openid = future_map[future] + try: + res = future.result(timeout=30) + if res.get('success'): + success += 1 + else: + fail += 1 + except Exception as e: + fail += 1 + logger.error(f'发送异常 {openid[:10]}...: {e}') + + logger.info(f'🏁 广播完成:成功 {success},失败 {fail},总计 {total}') + return { + 'success': True, + 'total': total, + 'success_count': success, + 'fail_count': fail + } \ No newline at end of file diff --git a/dingdan/urls.py b/dingdan/urls.py new file mode 100644 index 0000000..983d95e --- /dev/null +++ b/dingdan/urls.py @@ -0,0 +1,93 @@ +from django.urls import path +from django.views.decorators.csrf import csrf_exempt +from .views import CreateOrderView, WechatPayNotifyView, \ + PaymentFailView, PaymentVerifyView, DingdanHuoquView, DingdanXiangqingView, JiedanView, \ + ShangpinLeixingView, ShangjiaPaifaView, ShangjiaDingdanHuoquView, ShangjiaJiesuanView, \ + ShangjiaDingdanXiangqingView, ShangjiaChufaShenqingView, ShangjiaTuikuanShenqingView, \ + ShangjiaChexiaoView, DashouDingdanHuoquView, ShangpinLeixingHuoquView, QiangdanView, \ + DashouDingdanHuoquView1, DashouCOSZhengshuView, DashouTijiaoView, DashouDingdanXiangqingView, AdGetDingdanXiangQing, \ + AdTongYiTuiKuanPingTai, AdTongYiTuiKuanShangJia, AdQiangZhiJieDan, AdJuJueTuiKuan, AdGengHuanDaShou, \ + ShangjiaCOSZhengshuView, DashouHuoquLeixingView, AdJuJueJieSuan, AdZhuanYiDaTing, AdQuXiaoZhiDing, \ + PartnerNotifyClaimView, PartnerSyncOrderView, PartnerCheckClaimView, PartnerRefundNotifyView, DashouXiugaiView, \ + DaiLiQunLiaoXiaoXiView, VirtualCreateOrderView, VirtualPayNotifyView, VirtualPaymentVerifyView, \ + VirtualPaymentFailView, ShangjiaLianjieLiuYanZhuanFaView, DingdanXiangqingView2, JiedanView2, ZxsjghdsView, \ + ShangjiaFakuanApplyView, ShangjiaFakuaiXiugaiView + +urlpatterns = [ + path('xiadan', CreateOrderView.as_view(), name='下单订单生成接口'), + #path('pay-notify/', WechatPayNotifyView.as_view(), name='老板下单微信支付回调'), + path('pay-notify/', csrf_exempt(WechatPayNotifyView.as_view()), name='老板下单微信支付回调'), + path('shibai', PaymentFailView.as_view(), name='老板支付失败接口'), + path('fucha', PaymentVerifyView.as_view(), name='老板支付成功复查接口'), + path('dingdanhuoqu', DingdanHuoquView.as_view(), name='老板订单获取'), + path('dingdanxiangqing', DingdanXiangqingView.as_view(), name='老板订单详情获取'), + path('jiedan', JiedanView.as_view(), name='老板结单'), + path('sjspleixing', ShangpinLeixingView.as_view(), name='派单获取商品类型'), + path('sjpaifa', ShangjiaPaifaView.as_view(), name='商家发单'), + path('sjdingdanhq', ShangjiaDingdanHuoquView.as_view(), name='商家订单获取'), + path('sjddxq', ShangjiaDingdanXiangqingView.as_view(), name='商家订单详情获取'), + path('sjjiesuan', ShangjiaJiesuanView.as_view(), name='商家结单'), + path('sjchexiao', ShangjiaChexiaoView.as_view(), name='商家撤销订单'), + path('sjtkshenqing', ShangjiaTuikuanShenqingView.as_view(), name='商家退款申请'), + path('sjchufa', ShangjiaChufaShenqingView.as_view(), name='商家处罚打手申请'), + path('ddlxhq', ShangpinLeixingHuoquView.as_view(), name='打手抢单页商品类型获取'), + path('dshqdingdan', DashouDingdanHuoquView1.as_view(), name='打手订单列表页面获取'), + + # 获取订单列表 + path('ddhq', DashouDingdanHuoquView.as_view(), name='打手抢单页获取订单'), + path('qiangdan', QiangdanView.as_view(), name='打手抢单'), + path('dsscpz', DashouCOSZhengshuView.as_view(), name='打手上传图片OSS参数获取接口'), + # 打手提交订单完成接口 + path('dstijiao', DashouTijiaoView.as_view(), name='打手订单提交'), + # 打手订单详情获取接口 + path('dsddxq', DashouDingdanXiangqingView.as_view(), name='打手获取订单详情'), + path('adddxq', AdGetDingdanXiangQing.as_view(), name='管理员获取订单详情页面具体信息'), + path('adddpttk', AdTongYiTuiKuanPingTai.as_view(), name='同意平台订单退款'), + + # 商家订单退款接口(之前写的) + path('adddsjtk', AdTongYiTuiKuanShangJia.as_view(), name='同意商家退款'), + + # 强制结单接口 + path('adddqzjd', AdQiangZhiJieDan.as_view(), name='强制结单'), + + # 拒绝退款接口 + path('adddjj', AdJuJueTuiKuan.as_view(), name='拒绝退款'), + + # 更换打手接口 + path('adddgh', AdGengHuanDaShou.as_view(), name='管理更换打手'), + path('sjscpz', ShangjiaCOSZhengshuView.as_view(), name='商家上传证据图片获取密钥'), + path('dsqdhqddlx', DashouHuoquLeixingView.as_view(), name='打手抢单页面获取订单类型'), + + + path('adddjjjs', AdJuJueJieSuan.as_view(), name='管理员拒绝给打手钱'), + path('adddzydt', AdZhuanYiDaTing.as_view(), name='订单转移大厅'), + path('adddqxzd', AdQuXiaoZhiDing.as_view(), name='取消指定'), + + + path('partner_sync_order', csrf_exempt(PartnerSyncOrderView.as_view()), name='东道主派单接口'), + path('partner_check_claim', csrf_exempt(PartnerCheckClaimView.as_view()), name='对方抢单检查接口'), + + + path('partner_notify_claim', csrf_exempt(PartnerNotifyClaimView.as_view()), name='东道主订单被抢通知接口'), + path('partner_refund_notify', csrf_exempt(PartnerRefundNotifyView.as_view()), name='接受东道主退款通知'), + path('dsxiugaidd', DashouXiugaiView.as_view(), name='打手修改订单详情'), + + path('kptxxfs', DaiLiQunLiaoXiaoXiView.as_view(), name='前端聊天转发'), # 群聊消息代理发送 + + path('virtual_xiadan', VirtualCreateOrderView.as_view(),name='虚拟支付下单'), + path('virtual_notify', VirtualPayNotifyView.as_view(),name='虚拟支付下单回调'), + path('virtual_fucha', VirtualPaymentVerifyView.as_view(),name='虚拟支付下单成功复查'), + path('shibai', VirtualPaymentFailView.as_view(),name='虚拟支付下单失败'), # 或复用原 PaymentFailView + + path('sjljlbxxzf', ShangjiaLianjieLiuYanZhuanFaView.as_view(), name='商家链接老板聊天转发'), + + path('dingdanxiangqing2', DingdanXiangqingView2.as_view(), name='老板获取订单详情新街口'), + path('jiedan2', JiedanView2.as_view(), name='老板结单新街口'), + path('zxsjghds', ZxsjghdsView.as_view(), name='商家更换打手'), # 商家更换打手 + path('sjfksq', ShangjiaFakuanApplyView.as_view(), name='商家罚款'), + path('sjfkxgycx', ShangjiaFakuaiXiugaiView.as_view(), name='商家罚款和退款修改'), + + + + +] \ No newline at end of file diff --git a/dingdan/utils.py b/dingdan/utils.py new file mode 100644 index 0000000..14f4f18 --- /dev/null +++ b/dingdan/utils.py @@ -0,0 +1,441 @@ + +import threading +import requests + +from django.conf import settings +from peizhi.models import Club +from shangpin.models import ShangpinLeixing +from dingdan.models import Dingdan # 根据实际路径导入 + +import logging +from datetime import date + +from django.db import transaction +from django.db.models import F +from peizhi.models import DailyDispatchStat # 假设模型在 peizhi 应用中 + +logger = logging.getLogger(__name__) + +# 可选:线程池限制(避免大量线程同时创建,建议使用 Celery,这里简单用线程) +THREAD_POOL_SIZE = 10 +_semaphore = threading.Semaphore(THREAD_POOL_SIZE) + +def sync_order_to_partners(dingdan_id): + """ + 异步向所有开启互通的合作平台同步订单。 + 使用信号量限制并发线程数。 + """ + def _task(): + _semaphore.acquire() + try: + _sync_order_task(dingdan_id) + finally: + _semaphore.release() + + thread = threading.Thread(target=_task) + thread.daemon = True + thread.start() + +def _sync_order_task(dingdan_id): + """实际同步任务(在线程中执行)""" + try: + # 获取订单对象(只取必要字段,提升性能) + dingdan = Dingdan.objects.only( + 'dingdan_id', 'zhuangtai', 'jine', 'dashou_fencheng', 'jieshao', + 'beizhu', 'nicheng', 'leixing_id', 'yaoqiuleixing', 'huiyuan_id', + 'yongjin', 'zhiding_id', 'create_time' + ).get(dingdan_id=dingdan_id) + + # 基本条件判断 + if dingdan.zhuangtai != 1: + logger.info(f"订单 {dingdan_id} 状态 {dingdan.zhuangtai} 不是1,不同步") + return + if dingdan.zhiding_id: + logger.info(f"订单 {dingdan_id} 是指定单,不同步") + return + if dingdan.yaoqiuleixing != 1: + logger.info(f"订单 {dingdan_id} 抢单要求类型不是会员抢单,不同步") + return + if not dingdan.leixing_id: + logger.info(f"订单 {dingdan_id} 无商品类型,不同步") + return + try: + leixing = ShangpinLeixing.objects.get(id=dingdan.leixing_id) + if not leixing.is_cross_enabled: + logger.info(f"订单 {dingdan_id} 的商品类型未开启互通,不同步") + return + except ShangpinLeixing.DoesNotExist: + logger.info(f"订单 {dingdan_id} 的商品类型不存在,不同步") + return + + # 查询所有开启互通的俱乐部 + clubs = Club.objects.filter(is_interop_enabled=True).select_related() + if not clubs.exists(): + return + + # 为每个俱乐部发送订单 + for club in clubs: + _send_to_single_partner(dingdan, club) + + except Exception as e: + logger.error(f"同步订单任务异常: {e}", exc_info=True) + + + + + + +def _send_to_single_partner(dingdan, club): + """向单个俱乐部发送订单(带重试,幂等处理)""" + # 计算实际金额:订单打手分成 × 对方费率 + actual_amount = dingdan.dashou_fencheng * club.dashou_rate + actual_amount -= 1 + + # 构建请求数据 + payload = { + 'token': settings.CROSS_PLATFORM_TOKEN, + 'our_club_id': club.club_id, # 我方在对方系统中的标识(存储于对方表的 partner_club_id) + 'partner_club_id': club.partner_club_id, # 对方自己的俱乐部ID + 'partner_order_id': dingdan.dingdan_id, # 我方订单ID作为对方的外部订单号 + 'jine': str(actual_amount), # 对方平台应展示的订单金额 + 'dashou_fencheng': str(actual_amount), # 对方平台打手分成(与金额一致) + 'jieshao': dingdan.jieshao, + 'beizhu': dingdan.beizhu, + 'create_time': dingdan.create_time.isoformat(), + 'nicheng': dingdan.nicheng, + 'leixing_id': dingdan.leixing_id, # 我方商品类型ID,对方需要映射 + 'user_id': get_order_user_id(dingdan), # 对方平台下单用户ID(不带前缀) + } + + # 重试机制(最多3次,指数退避) + for attempt in range(1, 4): + try: + response = requests.post( + club.partner_domain + '/dingdan/partner_sync_order', + json=payload, + timeout=5 + ) + if response.status_code == 200: + data = response.json() + # 对方返回 code=0 表示成功(包括新建或已存在) + if data.get('code') == 0: + # 判断本次响应是新创建还是已存在 + status = data.get('data', {}).get('status', '') + if status == 'existed': + # 订单已经存在于对方系统,不重复统计 + logger.info(f"订单 {dingdan.dingdan_id} 在俱乐部 {club.club_id} 已存在,跳过统计") + else: + # 新创建成功,更新统计 + try: + update_daily_dispatch_stat( + direction=1, # 我方派单 + partner_club_id=club.partner_club_id, + dispatch_amount=actual_amount, + + ) + logger.info(f"成功同步订单 {dingdan.dingdan_id} 到俱乐部 {club.club_id}") + except Exception as e: + logger.error(f"更新派单统计失败: {e}", exc_info=True) + + # 标记订单为跨平台(仅当第一次成功时标记,避免重复标记) + if not dingdan.is_cross: + Dingdan.objects.filter(dingdan_id=dingdan.dingdan_id).update( + is_cross=1, + dispatch_type=1 # 我方派单 + ) + logger.info(f"订单 {dingdan.dingdan_id} 已标记为跨平台订单") + break + else: + # 对方返回业务错误(如映射不存在等),不重试 + logger.error(f"同步订单失败(业务): {data}") + break + else: + logger.error(f"同步订单HTTP失败: {response.status_code}") + except Exception as e: + logger.error(f"同步订单异常(尝试 {attempt}/3): {e}") + + if attempt < 3: + import time + time.sleep(2 ** attempt) # 指数退避 + else: + logger.error(f"同步订单 {dingdan.dingdan_id} 到俱乐部 {club.club_id} 最终失败") + + + + +# 获取下单方ID(根据订单类型) +def get_order_user_id(order): + from dingdan.models import DingdanPingtai, DingdanShangjia + if order.fadan_pingtai == 1: + try: + ext = DingdanPingtai.objects.get(dingdan=order) + return ext.laoban_id # 老板ID + except DingdanPingtai.DoesNotExist: + return None + elif order.fadan_pingtai == 2: + try: + ext = DingdanShangjia.objects.get(dingdan=order) + return ext.shangjia_id # 商家ID + except DingdanShangjia.DoesNotExist: + return None + return None + + + +# dingdan/utils/dispatch_stat.py +import logging + +import logging +from datetime import date +from decimal import Decimal +from django.db import transaction +from django.db.models import F +from peizhi.models import DailyDispatchStat # 请根据实际模型位置修改导入路径 + +logger = logging.getLogger(__name__) + + +def update_daily_dispatch_stat(direction, partner_club_id, + dispatch_amount=None, dispatch_count=1, + claimed_amount=None, claimed_count=1, + success_amount=None, success_count=1): + """ + 原子更新每日派单统计(支持派单、抢单、成交三种类型)。 + 只更新传递了非None参数的字段,未传递的字段保持不变。 + + 参数: + direction: 1=我方派单,2=对方派单 + partner_club_id: 对方俱乐部ID + dispatch_amount: 派单金额(Decimal),如果提供则累加到 dispatch_amount 和 dispatch_count + dispatch_count: 派单数量,默认为1,与 dispatch_amount 同时提供时生效 + claimed_amount: 抢单金额(Decimal),如果提供则累加到 claimed_amount 和 claimed_count + claimed_count: 抢单数量,默认为1,与 claimed_amount 同时提供时生效 + success_amount: 成交金额(Decimal),如果提供则累加到 success_amount 和 success_count + success_count: 成交数量,默认为1,与 success_amount 同时提供时生效 + """ + today = date.today() + year, month, day = today.year, today.month, today.day + + with transaction.atomic(): + # 使用 select_for_update 锁定行,避免并发累加冲突 + stat, created = DailyDispatchStat.objects.select_for_update().get_or_create( + date=today, + year=year, + month=month, + day=day, + direction=direction, + partner_club_id=partner_club_id, + defaults={ + 'dispatch_count': dispatch_count if dispatch_amount is not None else 0, + 'dispatch_amount': dispatch_amount or Decimal('0.00'), + 'claimed_count': claimed_count if claimed_amount is not None else 0, + 'claimed_amount': claimed_amount or Decimal('0.00'), + 'success_count': success_count if success_amount is not None else 0, + 'success_amount': success_amount or Decimal('0.00'), + } + ) + + if not created: + update_fields = [] + if dispatch_amount is not None: + stat.dispatch_count = F('dispatch_count') + dispatch_count + stat.dispatch_amount = F('dispatch_amount') + dispatch_amount + update_fields.extend(['dispatch_count', 'dispatch_amount']) + if claimed_amount is not None: + stat.claimed_count = F('claimed_count') + claimed_count + stat.claimed_amount = F('claimed_amount') + claimed_amount + update_fields.extend(['claimed_count', 'claimed_amount']) + if success_amount is not None: + stat.success_count = F('success_count') + success_count + stat.success_amount = F('success_amount') + success_amount + update_fields.extend(['success_count', 'success_amount']) + + if update_fields: + stat.save(update_fields=update_fields) + + logger.info(f"更新派单统计: 日期={today}, 方向={direction}, 俱乐部={partner_club_id}, " + f"派单+{dispatch_count if dispatch_amount else 0}单/{dispatch_amount or 0}, " + f"抢单+{claimed_count if claimed_amount else 0}单/{claimed_amount or 0}, " + f"成交+{success_count if success_amount else 0}单/{success_amount or 0}") + + + +from peizhi.models import DailyIncomeStat + +logger = logging.getLogger(__name__) + + +def update_daily_income(amount): + """ + 原子更新当日收入统计(金额累加,笔数加1)。 + 用于微信支付成功回调。 + + 参数: + amount: Decimal 本次收入金额 + """ + today = date.today() + year, month, day = today.year, today.month, today.day + + with transaction.atomic(): + stat, created = DailyIncomeStat.objects.select_for_update().get_or_create( + date=today, + defaults={ + 'year': year, + 'month': month, + 'day': day, + 'total_amount': amount, + 'total_count': 1 + } + ) + if not created: + # 原子累加 + stat.total_amount = F('total_amount') + amount + stat.total_count = F('total_count') + 1 + stat.save(update_fields=['total_amount', 'total_count']) + + logger.info( + f"每日收入更新: {today}, +{amount}元, 总金额={stat.total_amount if created else stat.total_amount + amount}, 总笔数={stat.total_count if created else stat.total_count + 1}") + + + +from peizhi.models import DailyPayoutStat + +logger = logging.getLogger(__name__) + +def update_daily_payout(amount): + """ + 原子更新当日出款统计(金额累加,笔数加1)。 + 用于提现成功、结算打款等场景。 + + 参数: + amount: Decimal 本次出款金额 + """ + today = date.today() + year, month, day = today.year, today.month, today.day + + with transaction.atomic(): + stat, created = DailyPayoutStat.objects.select_for_update().get_or_create( + date=today, + defaults={ + 'year': year, + 'month': month, + 'day': day, + 'total_amount': amount, + 'total_count': 1 + } + ) + if not created: + # 原子累加 + stat.total_amount = F('total_amount') + amount + stat.total_count = F('total_count') + 1 + stat.save(update_fields=['total_amount', 'total_count']) + + #logger.info(f"每日出款更新: {today}, +{amount}元, 总金额={stat.total_amount if created else stat.total_amount + amount}, 总笔数={stat.total_count if created else stat.total_count + 1}") + + +from decimal import Decimal + +from dingdan.models import Lilubiao + + +def calc_shangjia_order_fencheng(jine): + """ + 商家发单时计算打手/管事分成。 + 打手优先:打手+管事 > 订单金额时,管事分成为 0。 + """ + jine = Decimal(str(jine)) + try: + lilu_obj = Lilubiao.objects.get(fadanpingtai='3') + rate_dashou = Decimal(str(lilu_obj.lilu)) if lilu_obj.lilu and lilu_obj.lilu > 0 else Decimal('1') + except Lilubiao.DoesNotExist: + rate_dashou = Decimal('1') + + lilu_guanshi_obj = Lilubiao.objects.filter(fadanpingtai='13').first() + rate_guanshi = ( + Decimal(str(lilu_guanshi_obj.lilu)) + if lilu_guanshi_obj and lilu_guanshi_obj.lilu is not None + else Decimal('0') + ) + + dashou_fencheng = (jine * rate_dashou).quantize(Decimal('0.01')) + guanshi_raw = (jine * rate_guanshi).quantize(Decimal('0.01')) + + if dashou_fencheng + guanshi_raw > jine: + guanshi_fencheng = Decimal('0.00') + else: + guanshi_fencheng = guanshi_raw + + return dashou_fencheng, guanshi_fencheng + + +def settle_shangjia_order_guanshi_fenhong(order, dashou_id): + """ + 商家订单结单时结算管事分红(幂等,失败不抛异常阻断主流程)。 + """ + from shangpin.models import Gsfenhong + from yonghu.models import UserDashou, UserGuanshi + + if getattr(order, 'fadan_pingtai', None) != 2: + return + + guanshi_fencheng = order.guanshi_fencheng or Decimal('0') + if guanshi_fencheng <= 0: + return + + if Gsfenhong.objects.filter(dingdan_id=order.dingdan_id).exists(): + logger.info(f"商家订单管事分红已处理: {order.dingdan_id}") + return + + if not dashou_id: + return + + try: + dashou = UserDashou.objects.select_related('user').get(user__yonghuid=dashou_id) + except UserDashou.DoesNotExist: + logger.info(f"商家订单管事分红跳过: 打手{dashou_id}不存在") + return + + guanshi_id = dashou.yaoqingren + if not guanshi_id: + logger.info(f"商家订单管事分红跳过: 打手{dashou_id}无邀请管事") + return + + try: + with transaction.atomic(): + guanshi = UserGuanshi.objects.select_for_update().get(user__yonghuid=guanshi_id) + UserGuanshi.objects.filter(id=guanshi.id).update( + yue=F('yue') + guanshi_fencheng, + chongzhifenrun=F('chongzhifenrun') + guanshi_fencheng, + ) + + user_main = dashou.user + Gsfenhong.objects.create( + dingdan_id=order.dingdan_id, + guanshi=guanshi_id, + dashouid=dashou_id, + shuoming='商家订单分红', + fenhong=guanshi_fencheng, + avatar=user_main.avatar if user_main else None, + nicheng=dashou.nicheng or '未知打手', + fenhong_leixing=3, + ) + logger.info( + f"商家订单管事分红成功: 订单{order.dingdan_id}, 管事{guanshi_id}, " + f"打手{dashou_id}, 金额{guanshi_fencheng}元" + ) + except UserGuanshi.DoesNotExist: + logger.warning(f"商家订单管事分红跳过: 管事{guanshi_id}不存在") + return + except Exception as e: + logger.error(f"商家订单管事分红失败: {e}", exc_info=True) + return + + try: + from houtai.utils import update_guanshi_daily_by_action + update_guanshi_daily_by_action( + yonghuid=guanshi_id, + action=3, + amount=guanshi_fencheng, + ) + except Exception as e: + logger.error(f"商家订单管事日统计更新失败: {e}") \ No newline at end of file diff --git a/dingdan/views.py b/dingdan/views.py new file mode 100644 index 0000000..10cb6c9 --- /dev/null +++ b/dingdan/views.py @@ -0,0 +1,11602 @@ + +from tencentcloud.common.profile.client_profile import ClientProfile +from tencentcloud.common.profile.http_profile import HttpProfile + +from shangpin.models import Huiyuangoumai +from dingdan.models import Lilubiao, Pingfen, Chufajilu, Liaotian, Chufatupian + +from yonghu.models import UserShangjia +from houtai.utils import update_dashou_daily_by_action + +from decimal import Decimal +from houtai.utils import ( + update_shangjia_daily, + +) + +import logging +from rest_framework import permissions + +from dingdan.utils import sync_order_to_partners + +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__) + +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 + + + + + + + +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)''' + + # 创建订单平台扩展表 + """pingtaikuozhan_data = { + 'dingdan': dingdan, + 'laoban_id': laoban_yonghuid, + } + 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 + + + + + + +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 '游戏订单' + +# 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}) + + + + + + +""" +虚拟支付视图(代币模式) +完全基于原有商户支付逻辑,仅替换支付参数生成和回调/查单方式 +数据库字段、状态流转、业务规则 100% 保持一致 +""" +import json +import hmac +import hashlib +import logging +import time +import random +from decimal import Decimal + +import requests +from django.conf import settings +from django.db import transaction +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import permissions +from rest_framework.parsers import JSONParser +from django.http import HttpResponse +import xmltodict +import threading + +from .models import Dingdan, DingdanPingtai, Lilubiao +from shangpin.models import Shangpin +from yonghu.models import UserMain, UserDashou, UserBoss +from peizhi.models import Szjilu +from .utils import update_daily_income, sync_order_to_partners + +logger = logging.getLogger(__name__) + + +class VirtualCreateOrderView(APIView): + """ + 虚拟代币支付创建订单 + 流程与原 CreateOrderView 完全一致,仅支付参数生成改为代币虚拟支付 + """ + 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) + code = request.data.get('code') # 虚拟支付必须每单都用最新 code 换 session_key + + # 2. 临时关闭指定功能(与原逻辑一致) + if zhiding: + return Response({'code': 8, 'msg': '指定功能尚未开启,请不要填写指定打手ID', 'data': None}) + + # 3. 验证必填字段 + 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}) + if not code: + return Response({'code': 10, 'msg': '缺少登录凭证code', 'data': None}) + + # 4. 查询商品信息 + shangpin = Shangpin.objects.filter(id=shangpin_id).first() + if not shangpin: + return Response({'code': 4, '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) + + # 5. 验证指定打手(如果存在)---- 虽然前面已禁止,保留原逻辑以备后续开启 + zhiding_id_value = None + if zhiding: + zhiding_user = UserMain.objects.filter(yonghuid=zhiding).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 + + # 6. 生成订单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}" + + # 7. 查询利率并计算分成(完全复用原代码) + dashou_lilu = Lilubiao.objects.filter(fadanpingtai='1').first() + if not dashou_lilu: + return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None}) + + 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)) + + # 8. 强制刷新 session_key(虚拟支付签名所必需) + wx_resp = self._exchange_code_for_session_key(code) + if wx_resp is None: + return Response({'code': 11, 'msg': '微信认证失败', 'data': None}) + + session_key = wx_resp['session_key'] + user_openid = wx_resp['openid'] + logger.info(f"[VirtualCreate] session_key刷新成功,openid={user_openid}") + + # 更新用户 openid 和 session_key(保持数据库最新) + current_user = request.user + current_user.openid = user_openid + current_user.session_key = session_key + current_user.save(update_fields=['openid', 'session_key']) + + # 9. 数据库事务(与原代码完全一致) + with transaction.atomic(): + laoban_yonghuid = getattr(current_user, 'yonghuid', '') + + # 创建订单主表 + 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{current_user.yonghuid}", + 'leixing_id': shangpin.leixing_id + } + dingdan = Dingdan.objects.create(**order_data) + + # 创建订单平台扩展表 + DingdanPingtai.objects.create( + dingdan=dingdan, + laoban_id=laoban_yonghuid, + ) + + # 生成代币虚拟支付参数 + pay_params = self.generate_virtual_pay_params( + dingdanid=dingdanid, + jine=shangpin_jiage, + openid=user_openid, + session_key=session_key, + beizhu=beizhu + ) + + if not pay_params: + raise Exception('虚拟支付参数生成失败') + + return Response({ + 'code': 0, + 'msg': '订单创建成功', + 'data': { + 'dingdanid': dingdanid, + 'payParams': pay_params + } + }) + + except Exception as e: + logger.error(f"虚拟支付下单失败: {e}", exc_info=True) + return Response({'code': 99, 'msg': '系统错误', 'data': None}) + + def _exchange_code_for_session_key(self, code): + """通过 jscode2session 换取 openid 和 session_key""" + url = 'https://api.weixin.qq.com/sns/jscode2session' + params = { + 'appid': settings.WEIXIN_APPID, + 'secret': settings.WEIXIN_SECRET, + 'js_code': code, + 'grant_type': 'authorization_code' + } + try: + resp = requests.get(url, params=params, timeout=5) + data = resp.json() + if 'openid' in data and 'session_key' in data: + return data + else: + logger.error(f"jscode2session 失败: {data}") + except Exception as e: + logger.error(f"换取 session_key 异常: {e}") + return None + + def generate_virtual_pay_params(self, dingdanid, jine, openid, session_key, beizhu=''): + """生成代币虚拟支付参数""" + env = settings.VP_ENV + appkey = settings.VP_APPKEY_SANDBOX if env == 1 else settings.VP_APPKEY + offer_id = settings.VP_OFFER_ID + product_id = 'coin_1' # 代币道具ID,已配置 + + # 代币购买数量 = 总价(分) + buy_quantity = int(float(jine) * 100) + + # 构建 signData(驼峰字段,代币模式不含 goodsPrice) + sign_data = { + "offerId": offer_id, + "env": env, + "openid": openid, + "outTradeNo": dingdanid, + "productId": product_id, + "buyQuantity": buy_quantity, + "attach": beizhu or '游戏陪玩订单', + "currencyType": "CNY", + "zoneId": "1" + } + sign_data_str = json.dumps(sign_data, separators=(',', ':'), ensure_ascii=False) + + # 签名计算 + uri = "requestVirtualPayment" + msg = uri + "&" + sign_data_str + pay_sig = hmac.new(appkey.encode('utf-8'), msg.encode('utf-8'), hashlib.sha256).hexdigest() + signature = hmac.new(session_key.encode('utf-8'), sign_data_str.encode('utf-8'), hashlib.sha256).hexdigest() + + return { + "signData": sign_data_str, + "paySig": pay_sig, + "signature": signature, + "mode": "short_series_coin" + } + + +class VirtualPayNotifyView(APIView): + """ + 虚拟支付回调接口 + 业务处理逻辑与原 WechatPayNotifyView 完全一致, + 仅 XML 解析和签名验证方式不同 + """ + permission_classes = [permissions.AllowAny] + + def post(self, request): + try: + # 1. 验证签名(虚拟支付回调使用 HMAC-SHA256) + body = request.body + signature = request.headers.get('Wechatpay-Signature') + if not self._verify_virtual_signature(body, signature): + logger.warning("虚拟支付回调签名验证失败") + return HttpResponse('FAIL', status=401) + + # 2. 解析 XML 通知 + data = xmltodict.parse(body.decode('utf-8')).get('xml', {}) + event = data.get('Event') + if event != 'xpay_goods_deliver_notify': + return HttpResponse('success') + + out_trade_no = data.get('OutTradeNo') + transaction_id = '' + wechat_pay_info = data.get('WeChatPayInfo', {}) + if isinstance(wechat_pay_info, dict): + transaction_id = wechat_pay_info.get('TransactionId', '') + + # 3. 执行支付成功处理(复用原 _handle_success 逻辑) + self._handle_success(out_trade_no, transaction_id) + return HttpResponse('success') + + except Exception as e: + logger.error(f"虚拟支付回调异常: {e}", exc_info=True) + return HttpResponse('success') + + def _verify_virtual_signature(self, body, signature): + """验证虚拟支付回调签名""" + if not signature: + return False + appkey = settings.VP_APPKEY_SANDBOX if settings.VP_ENV == 1 else settings.VP_APPKEY + calc_sign = hmac.new(appkey.encode('utf-8'), body, hashlib.sha256).hexdigest() + return hmac.compare_digest(calc_sign, signature) + + @transaction.atomic + def _handle_success(self, out_trade_no, transaction_id): + """支付成功处理(与原 WechatPayNotifyView._handle_success 完全相同)""" + logger.info(f"虚拟支付回调成功, 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 + + # 幂等处理 + if dingdan.zhuangtai != 9: + logger.info(f"订单 {out_trade_no} 已处理过,状态={dingdan.zhuangtai},跳过") + return + + # 保存交易号 + dingdan.wechat_transaction_id = transaction_id or 'virtual_' + out_trade_no + + # 更新状态 + if dingdan.zhiding_id and dingdan.zhiding_id.strip(): + dingdan.zhuangtai = 7 + else: + dingdan.zhuangtai = 1 + dingdan.save() + + # 废弃同商品其他未支付订单 + Dingdan.objects.filter( + user1_id=dingdan.user1_id, + shangpin_id=dingdan.shangpin_id, + zhuangtai=9 + ).exclude(dingdan_id=out_trade_no).update(zhuangtai=25) + + # 更新老板数据 + try: + 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() + except Exception as e: + logger.error(f"更新老板数据异常: {e}") + + # 更新商品库存 + if dingdan.shangpin_id: + Shangpin.objects.filter(id=dingdan.shangpin_id).update(kucun=F('kucun') - 1) + + # 更新收支记录 + self._update_szjilu(dingdan.jine) + update_daily_income(dingdan.jine) + sync_order_to_partners(dingdan.dingdan_id) + + # 异步广播 + self._broadcast_async(dingdan) + + logger.info(f"订单 {out_trade_no} 支付成功处理完成") + + def _update_szjilu(self, jine): + """更新收支记录表(与原逻辑一致)""" + 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') + } + ) + szjilu.zongsy += jine_decimal + szjilu.zongls += jine_decimal + szjilu.jrls += jine_decimal + szjilu.save() + except Exception as e: + logger.error(f"更新收支记录失败: {e}") + + def _broadcast_async(self, dingdan): + """异步广播(与原一致)""" + try: + def task(): + try: + from . 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], + } + sender = WeixinBroadcastSender() + sender.broadcast_order(order_info) + except Exception as e: + logger.error(f"广播任务异常: {e}") + + thread = threading.Thread(target=task) + thread.daemon = True + thread.start() + 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: + return '游戏订单' + + +class VirtualPaymentVerifyView(APIView): + """ + 虚拟支付复查接口 + 逻辑与原 PaymentVerifyView 完全一致,仅查单方式改为虚拟支付查单 API + """ + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 1, 'msg': '订单ID不能为空'}) + + dingdan = Dingdan.objects.filter(dingdan_id=dingdanid).first() + if not dingdan: + return Response({'code': 2, 'msg': '订单不存在'}) + + current_user = request.user + user_identifier = f"Boss{current_user.yonghuid}" + if dingdan.user1_id != user_identifier: + return Response({'code': 403, 'msg': '无权访问'}) + + # 状态已是成功则直接返回 + if dingdan.zhuangtai in [1, 7]: + return Response({ + 'code': 0, + 'msg': '订单状态正常', + 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai} + }) + + # 状态为未支付,主动查单 + if dingdan.zhuangtai == 9: + from django.core.cache import cache + lock_key = f"virtual_verify_lock_{dingdanid}" + if not cache.add(lock_key, "locked", timeout=10): + return Response({'code': 1, 'msg': '订单正在处理中,请稍后'}) + + try: + trade_state, transaction_id = self._query_virtual_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: + return Response({'code': 2, 'msg': '支付失败'}) + except Exception as e: + logger.error(f"查询虚拟支付异常: {e}") + return Response({'code': 1, 'msg': '查询失败,请稍后重试'}) + finally: + cache.delete(lock_key) + + # 其他异常状态 + status_msg = { + 2: '订单已取消', 3: '订单已完成', 4: '退款审核中', + 5: '已退款', 6: '退款失败', 8: '结算中', 0: '订单已作废' + } + return Response({'code': 3, 'msg': status_msg.get(dingdan.zhuangtai, '订单状态异常'), + 'data': {'zhuangtai': dingdan.zhuangtai}}) + + except Exception as e: + logger.error(f"复查接口异常: {e}", exc_info=True) + return Response({'code': 99, 'msg': '系统错误'}) + + @transaction.atomic + def _process_payment_success(self, dingdan, transaction_id, operator_id): + """支付成功原子操作,与旧流程完全一致""" + 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 + dingdan.save() + + 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) + + self._update_szjilu(dingdan.jine) + update_daily_income(dingdan.jine) + sync_order_to_partners(dingdan.dingdan_id) + + def _query_virtual_payment(self, out_trade_no): + """调用虚拟支付查单接口""" + uri = "/xpay/query_order" + appkey = settings.VP_APPKEY_SANDBOX if settings.VP_ENV == 1 else settings.VP_APPKEY + offer_id = settings.VP_OFFER_ID + openid = self.request.user.openid + + post_data = { + "offer_id": offer_id, + "openid": openid, + "out_trade_no": out_trade_no, + "env": settings.VP_ENV, + } + body = json.dumps(post_data, separators=(',', ':')) + msg = uri + "&" + body + pay_sig = hmac.new(appkey.encode(), msg.encode(), hashlib.sha256).hexdigest() + + headers = {'Content-Type': 'application/json'} + url = f"https://api.weixin.qq.com{uri}?pay_sig={pay_sig}" + resp = requests.post(url, data=body, headers=headers, timeout=10) + result = resp.json() + + if result.get('errcode') == 0: + order = result.get('order', {}) + state = order.get('state', 1) # 1=未支付, 2=已支付 + if state == 2: + return 'SUCCESS', order.get('transaction_id', '') + elif state == 1: + return 'USERPAYING', '' + else: + return 'CLOSED', '' + else: + raise Exception(f"虚拟支付查单失败: {result.get('errmsg')}") + + def _update_szjilu(self, jine): + """更新收支记录(同原)""" + 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') + } + ) + szjilu.zongsy += jine_decimal + szjilu.zongls += jine_decimal + szjilu.jrls += jine_decimal + szjilu.save() + except Exception as e: + logger.error(f"更新收支记录失败: {e}") + + +# 保留原有支付失败处理接口(若需要,可直接复用原 PaymentFailView) +# 这里提供一份与旧版一致的实现,确保前端调用 /dingdan/shibai 依旧可用 +class VirtualPaymentFailView(APIView): + """ + 支付失败处理(与旧 PaymentFailView 逻辑一致) + 仅删除状态为9的订单 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 1, 'msg': '订单ID不能为空'}) + + with transaction.atomic(): + dingdan = Dingdan.objects.select_for_update().filter(dingdan_id=dingdanid).first() + if not dingdan: + return Response({'code': 2, 'msg': '订单不存在'}) + if dingdan.zhuangtai != 9: + return Response({'code': 3, 'msg': '只能删除未支付的订单'}) + + DingdanPingtai.objects.filter(dingdan=dingdan).delete() + dingdan.delete() + + return Response({'code': 0, 'msg': '订单已删除'}) + except Exception as e: + return Response({'code': 99, 'msg': f'删除订单失败: {str(e)}'}) + + + + + + + + +import json +import hmac +import hashlib +import logging +import time +import random +from decimal import Decimal + +import requests +from django.conf import settings +from django.db import transaction +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import permissions +from rest_framework.parsers import JSONParser +from django.http import HttpResponse +import xmltodict +import threading + +from .models import Dingdan, DingdanPingtai, Lilubiao +from shangpin.models import Shangpin +from yonghu.models import UserMain, UserDashou +from peizhi.models import Szjilu +from .utils import update_daily_income, sync_order_to_partners + +logger = logging.getLogger(__name__) + + +class VirtualCreateOrderView(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) + + if zhiding: + return Response({'code': 8, 'msg': '指定功能尚未开启', 'data': None}) + + if not shangpin_id: + return Response({'code': 1, 'msg': '商品ID不能为空', 'data': None}) + if not nicheng: + return Response({'code': 2, 'msg': '游戏昵称不能为空', 'data': None}) + + # 2. 查商品 + shangpin = Shangpin.objects.filter(id=shangpin_id).first() + if not shangpin: + return Response({'code': 4, '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) + + # 3. 生成订单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}" + + # 4. 计算分成(保留原有逻辑) + dashou_lilu = Lilubiao.objects.filter(fadanpingtai='1').first() + if not dashou_lilu: + return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None}) + 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)) + + with transaction.atomic(): + current_user = request.user + 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': None, + '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{current_user.yonghuid}", + 'leixing_id': shangpin.leixing_id + } + dingdan = Dingdan.objects.create(**order_data) + DingdanPingtai.objects.create( + dingdan=dingdan, + laoban_id=current_user.yonghuid, + ) + + # 获取或刷新 session_key + session_key = getattr(current_user, 'session_key', None) + if not session_key: + code = request.data.get('code') + if not code: + raise Exception('缺少登录凭证code') + wx_resp = self._exchange_code_for_session_key(code) + if wx_resp is None or wx_resp.get('openid') != user_openid: + raise Exception('微信认证失败') + session_key = wx_resp['session_key'] + current_user.session_key = session_key + current_user.save(update_fields=['session_key']) + + # 生成支付参数 + pay_params = self.generate_virtual_pay_params( + dingdanid=dingdanid, + jine=shangpin_jiage, + openid=user_openid, + session_key=session_key, + beizhu=beizhu + ) + + return Response({ + 'code': 0, + 'msg': '订单创建成功', + 'data': { + 'dingdanid': dingdanid, + 'payParams': pay_params + } + }) + + except Exception as e: + logger.error(f"虚拟支付下单失败: {e}", exc_info=True) + return Response({'code': 99, 'msg': f'系统错误', 'data': None}) + + def _exchange_code_for_session_key(self, code): + url = 'https://api.weixin.qq.com/sns/jscode2session' + params = { + 'appid': settings.WEIXIN_APPID, + 'secret': settings.WEIXIN_SECRET, + 'js_code': code, + 'grant_type': 'authorization_code' + } + try: + resp = requests.get(url, params=params, timeout=5) + data = resp.json() + if 'openid' in data and 'session_key' in data: + return data + except Exception as e: + logger.error(f"换取 session_key 失败: {e}") + return None + + def generate_virtual_pay_params(self, dingdanid, jine, openid, session_key, beizhu=''): + """生成虚拟支付签名参数""" + env = settings.VP_ENV + appkey = settings.VP_APPKEY_SANDBOX if env == 1 else settings.VP_APPKEY + offer_id = settings.VP_OFFER_ID + product_id = settings.VP_PRODUCT_ID + + # 金额转换:元 → 分 + goods_price_fen = int(float(jine) * 100) + + # signData —— 必须用驼峰字段 !!! + sign_data = { + "offerId": offer_id, + "env": env, + "openid": openid, + "outTradeNo": dingdanid, + "productId": product_id, + "buyQuantity": 1, # 数量固定为 1 + "goodsPrice": goods_price_fen, # 道具真实价格(分) + "attach": beizhu or '游戏陪玩订单', + "currencyType": "CNY", + "zoneId": "1" + } + sign_data_str = json.dumps(sign_data, separators=(',', ':'), ensure_ascii=False) + + # 签名 + uri = "requestVirtualPayment" + msg = uri + "&" + sign_data_str + pay_sig = hmac.new(appkey.encode('utf-8'), msg.encode('utf-8'), hashlib.sha256).hexdigest() + signature = hmac.new(session_key.encode('utf-8'), sign_data_str.encode('utf-8'), hashlib.sha256).hexdigest() + + # 返回给前端,这三个字段必须存在:signData、paySig、signature + return { + "signData": sign_data_str, + "paySig": pay_sig, + "signature": signature, + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +import string + +import xmltodict +import logging + +from django.db import transaction, models + +from rest_framework import permissions, status +from rest_framework.parsers import JSONParser + +from shangpin.models import Shangpin + + +logger = logging.getLogger(__name__) + + + +from django.core.cache import cache + +import json +import hmac +import hashlib +import logging +import time +import traceback +from decimal import Decimal + +import requests +from django.conf import settings +from django.db import transaction, models +from rest_framework import permissions +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework.parsers import JSONParser + + + +logger = logging.getLogger(__name__) + + +class VirtualCreateOrderView(APIView): + """虚拟支付 - 创建订单(逻辑与旧下单 100% 相同)""" + 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).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. 分成计算(与原逻辑完全相同) + dashou_lilu = Lilubiao.objects.filter(fadanpingtai='1').first() + if not dashou_lilu: + return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None}) + + 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)) + + # 7. 数据库事务(创建订单 + 扩展表 + 生成虚拟支付参数) + 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}) + + # 订单主表数据(字段与原订单 100% 一致) + 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) + + # 扩展表(与原逻辑完全相同) + dianpu_id, pingtai_shouyi, dianpu_shouyi = 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, + } + 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) + + # ★ 唯一改动:生成虚拟支付签名 + pay_params = self.generate_virtual_pay_params( + dingdanid=dingdanid, + jine=shangpin_jiage, + user_ip=self.get_client_ip(request), + openid=user_openid, + request=request, + beizhu=beizhu, + ) + + if not pay_params: + raise Exception('虚拟支付参数生成失败') + + return Response({ + 'code': 0, + 'msg': '订单创建成功', + 'data': { + 'dingdanid': dingdanid, + 'payParams': pay_params, # 结构适配 wx.requestVirtualPayment + } + }) + + except Exception as e: + logger.error(f"虚拟支付下单失败: {e}", exc_info=True) + return Response({'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None}) + + def get_client_ip(self, request): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + return x_forwarded_for.split(',')[0] + return request.META.get('REMOTE_ADDR', '127.0.0.1') + + def generate_virtual_pay_params(self, dingdanid, jine, user_ip, openid, request, beizhu=''): + """生成虚拟支付所需签名和参数(完全符合微信官方规范)""" + import random, string + + env = settings.VP_ENV + appkey = settings.VP_APPKEY_SANDBOX if env == 1 else settings.VP_APPKEY + offer_id = settings.VP_OFFER_ID + + # 获取 session_key(必须,请在 UserMain 模型中添加 session_key 字段,登录时存储) + current_user = request.user + session_key = getattr(current_user, 'session_key', None) + if not session_key: + raise Exception('用户session_key缺失,请确保登录时存储了session_key') + + total_fee = int(float(jine) * 100) # 金额:分 + + # 构建 signData(决定签名内容,字段顺序、JSON格式必须一致) + sign_data = { + "offer_id": offer_id, + "env": env, + "openid": openid, + "out_trade_no": dingdanid, + "product_id": "1", # 默认道具ID,可后续按商品配置 + "amount": total_fee, + "attach": beizhu or '游戏陪玩订单', + "user_ip": user_ip, + "currency_type": "CNY", + "zone_id": "1", + } + sign_data_str = json.dumps(sign_data, separators=(',', ':'), ensure_ascii=False) + + # 计算 pay_sig(支付签名) + uri = "requestVirtualPayment" + msg = uri + "&" + sign_data_str + pay_sig = hmac.new(appkey.encode('utf-8'), msg.encode('utf-8'), hashlib.sha256).hexdigest() + + # 计算 signature(用户态签名) + signature = hmac.new( + session_key.encode('utf-8'), + sign_data_str.encode('utf-8'), + hashlib.sha256 + ).hexdigest() + + return { + "offerId": offer_id, + "env": env, + "zoneId": "1", + "outTradeNo": dingdanid, + "productId": "1", + "amount": total_fee, # 单位:分 + "signData": sign_data_str, # JSON字符串 + "paySig": pay_sig, + "signature": signature, + } + + +class VirtualPayNotifyView(APIView): + """虚拟支付 - 支付成功推送处理(替代原 WechatPayNotifyView)""" + permission_classes = [permissions.AllowAny] + + def post(self, request): + try: + body = request.body.decode('utf-8') + # 微信推送可能是 XML,也可能是 JSON,这里按 XML 解析(兼容主流情况) + import xmltodict + data = xmltodict.parse(body)['xml'] + + event = data.get('Event') + if event != 'xpay_goods_deliver_notify': + return HttpResponse('success') + + out_trade_no = data.get('OutTradeNo') + wechat_pay_info = data.get('WeChatPayInfo', {}) + transaction_id = '' + if isinstance(wechat_pay_info, dict): + transaction_id = wechat_pay_info.get('TransactionId', '') + + self._handle_payment_success(out_trade_no, transaction_id) + return HttpResponse('success') + + except Exception as e: + logger.error(f"虚拟支付回调异常: {e}", exc_info=True) + # 即使出错也返回 success,防止微信重复推送 + return HttpResponse('success') + + @transaction.atomic + def _handle_payment_success(self, out_trade_no, transaction_id): + """支付成功处理,与原 WechatPayNotifyView._handle_success 完全相同""" + dingdan = Dingdan.objects.select_for_update().filter(dingdan_id=out_trade_no).first() + if not dingdan or dingdan.zhuangtai != 9: + logger.info(f"订单 {out_trade_no} 已处理或不存在,状态: {getattr(dingdan, 'zhuangtai', 'N/A')}") + return + + # 保存交易号 + dingdan.wechat_transaction_id = transaction_id or ('virtual_' + out_trade_no) + + # 状态更新 + if dingdan.zhiding_id and dingdan.zhiding_id.strip(): + dingdan.zhuangtai = 7 + else: + dingdan.zhuangtai = 1 + dingdan.save() + + # 废弃同一用户同一商品下其他未支付订单 + Dingdan.objects.filter( + user1_id=dingdan.user1_id, + shangpin_id=dingdan.shangpin_id, + zhuangtai=9, + ).exclude(dingdan_id=out_trade_no).update(zhuangtai=25) + + # 更新老板消费数据 + try: + 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() + except Exception as e: + logger.error(f"更新老板数据失败: {e}") + + # 更新商品库存 + if dingdan.shangpin_id: + Shangpin.objects.filter(id=dingdan.shangpin_id).update(kucun=F('kucun') - 1) + + # 商品/店铺每日统计 + try: + 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, + dingdan_jiage=dingdan.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi, + ) + dianpu_id = dingdan.pingtai_kuozhan.dianpu_id if hasattr(dingdan, 'pingtai_kuozhan') else None + 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, + dingdan_jiage=dingdan.jine, + dianpu_shouyi=dianpu_shouyi, + ) + except Exception as e: + logger.error(f"统计更新失败: {e}") + + # 收支记录 + self._update_szjilu(dingdan.jine) + update_daily_income(dingdan.jine) + sync_order_to_partners(dingdan.dingdan_id) + + def _update_szjilu(self, jine): + 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'), + } + ) + szjilu.zongsy += jine_decimal + szjilu.zongls += jine_decimal + szjilu.jrls += jine_decimal + szjilu.save() + except Exception as e: + logger.error(f"收支记录更新失败: {e}") + + +class VirtualPaymentVerifyView(APIView): + """ + 虚拟支付 - 复查接口(支付成功后前端调用确认订单状态) + 与原 PaymentVerifyView 逻辑完全相同,仅查询支付状态的接口改为虚拟支付 + """ + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 1, 'msg': '订单ID不能为空'}) + + dingdan = Dingdan.objects.filter(dingdan_id=dingdanid).first() + if not dingdan: + return Response({'code': 2, 'msg': '订单不存在'}) + + current_user = request.user + if dingdan.user1_id != f"Boss{current_user.yonghuid}": + return Response({'code': 403, 'msg': '无权访问'}) + + # 若状态已是已支付,直接返回 + if dingdan.zhuangtai in [1, 7]: + return Response({ + 'code': 0, + 'msg': '订单状态正常', + 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai}, + }) + + if dingdan.zhuangtai == 9: + # 主动查询虚拟支付订单状态 + trade_state, transaction_id = self._query_virtual_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: + return Response({'code': 2, 'msg': '支付失败'}) + else: + status_msg = { + 2: '订单已取消', 3: '订单已完成', 4: '退款审核中', + 5: '已退款', 6: '退款失败', 8: '结算中', 0: '订单已作废', + } + return Response({'code': 3, 'msg': status_msg.get(dingdan.zhuangtai, '订单状态异常')}) + + except Exception as e: + logger.error(f"复查异常: {e}", exc_info=True) + return Response({'code': 99, 'msg': f'系统错误: {str(e)}'}) + + def _query_virtual_payment(self, out_trade_no): + """调用 /xpay/query_order 查询虚拟支付订单状态""" + uri = "/xpay/query_order" + appkey = settings.VP_APPKEY_SANDBOX if settings.VP_ENV == 1 else settings.VP_APPKEY + offer_id = settings.VP_OFFER_ID + openid = self.request.user.openid + + post_data = { + "offer_id": offer_id, + "openid": openid, + "out_trade_no": out_trade_no, + "env": settings.VP_ENV, + } + body = json.dumps(post_data, separators=(',', ':')) + + # 计算签名 + msg = uri + "&" + body + pay_sig = hmac.new(appkey.encode(), msg.encode(), hashlib.sha256).hexdigest() + + headers = {'Content-Type': 'application/json'} + url = f"https://api.weixin.qq.com{uri}" + resp = requests.post(url, data=body, headers=headers, timeout=10) + result = resp.json() + + if result.get('errcode') == 0: + order = result.get('order', {}) + state = order.get('state', 1) + if state == 2: + return 'SUCCESS', order.get('transaction_id', '') + elif state == 1: + return 'USERPAYING', '' + else: + return 'CLOSED', '' + raise Exception(f"查单失败: {result.get('errmsg', '未知错误')}") + + @transaction.atomic + def _process_payment_success(self, dingdan, transaction_id, operator_id): + """与回调中的处理逻辑 100% 一致,保证事务原子性""" + 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 or ('virtual_' + dingdan.dingdan_id) + dingdan.clkf = operator_id + dingdan.save() + + 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: + if dingdan.shangpin_id: + 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, + dingdan_jiage=dingdan.jine, pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi, + ) + dianpu_id = dingdan.pingtai_kuozhan.dianpu_id if hasattr(dingdan, 'pingtai_kuozhan') else None + 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, + dingdan_jiage=dingdan.jine, dianpu_shouyi=dianpu_shouyi, + ) + except Exception as e: + logger.error(f"复查统计失败: {e}") + + self._update_szjilu(dingdan.jine) + update_daily_income(dingdan.jine) + sync_order_to_partners(dingdan.dingdan_id) + + def _update_szjilu(self, jine): + 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'), + } + ) + szjilu.zongsy += jine_decimal + szjilu.zongls += jine_decimal + szjilu.jrls += jine_decimal + szjilu.save() + except Exception as e: + logger.error(f"复查收支更新失败: {e}") + + + + + + + + + + + + + + + + +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) + + +'''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) + # 重新获取订单以获取最新状态(因为_process_payment_success可能修改了dingdan) + 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 _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 _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() + + # 删除同一用户同一商品的其他未支付订单(避免僵尸订单) + Dingdan.objects.filter( + user1_id=dingdan.user1_id, + shangpin_id=dingdan.shangpin_id, + zhuangtai=9 + ).exclude(dingdan_id=dingdan.dingdan_id).delete() + + # 更新老板扩展表 + laoban_id = dingdan.pingtai_kuozhan.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() + + # 更新商品库存 + Shangpin.objects.filter(id=dingdan.shangpin_id).update( + kucun=models.F('kucun') - 1 + ) + + # 更新收支记录 + self._update_szjilu(dingdan.jine) + + 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)''' + + + + + +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated + + +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: + + 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 + + + + + + +# dingdan/views.py + + +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) + + +# views/order_views.py - ShangjiaPaifaView完整代码 +import os +import logging +from shangpin.models import ShangpinLeixing +import threading +from utils.weixin_broadcast import WeixinBroadcastSender + + +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}") + + # 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 = 派发 + ) + + + self.broadcast_order_notification_async(dingdan) + + # ========== 🆕 新增:判断是否需要跨平台同步 ========== + + sync_order_to_partners(dingdan.dingdan_id) + + + # 记录成功日志 + 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: + game_type = ShangpinLeixing.objects.filter(id=leixing_id).first() + return game_type.jieshao if game_type else '游戏订单' + except Exception: + 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_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)''' + + + + +# views.py 中的 ShangjiaDingdanXiangqingView 完整改造 + + + +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 + + + +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: + 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 # 发送我方订单ID,对方用 partner_order_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 + + + +'''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, + } + })''' + +# 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) + } + }) + + + + + + + + + + + + +# dingdan/views.py +from django.db.models import Q, OuterRef, Subquery, F, Prefetch + +from dengji.models import DingdanBiaoqian +from dengji.models import YonghuChenghao, Chenghao +from rest_framework_simplejwt.authentication import JWTAuthentication + +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. 标签筛选 + if biaoqian_id and int(biaoqian_id) > 0: + base_query &= Q(dingdanbiaoqian__chenghao_id=int(biaoqian_id)) + + # 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. 获取当前页数据,并通过 select_related 预取商家扩展表(反向关联) + page_qs = base_qs[offset:offset + limit].select_related('shangjia_kuozhan') + + # 8. 获取当前页所有需要的字段 + dingdan_values = page_qs.values( + 'dingdan_id', 'zhuangtai', 'fadan_pingtai', 'dashou_fencheng', + 'zhiding_id', 'leixing_id', 'tupian', 'yaoqiuleixing', + 'huiyuan_id', 'yongjin', 'jieshao', 'beizhu', 'create_time', + 'shangjia_kuozhan__sjnicheng', # 商家昵称 + 'shangjia_kuozhan__shangjia_id', # 商家ID + ) + + # 9. 转换为列表并格式化 + dingdan_list = [] + for order in dingdan_values: + dingdan_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['shangjia_kuozhan__sjnicheng'] or '', + 'sj_id': order['shangjia_kuozhan__shangjia_id'] or '', + # 其他字段后面补充 + }) + + # 10. 批量获取指定打手的头像和昵称 + 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 + } + + # 11. 批量获取三层标签(不变) + 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) + + # --- 用户标签(打手+商家) --- + yonghuid_set = set(zhiding_ids) + # 商家ID从订单列表中提取(已含 sj_id) + for item in dingdan_list: + if item['fadan_pingtai'] == 2 and item.get('sj_id'): + yonghuid_set.add(item['sj_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: + sj_yonghuid = item.get('sj_id') + 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 item in dingdan_list: + oid = item['dingdan_id'] + zid = item.get('zhiding_id') + info = zhiding_info.get(zid, {}) + formatted_list.append({ + **item, + 'zhiding_uid': zid, + '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 {} + + + + + + +import logging + +logger = logging.getLogger(__name__) + + + + +from rest_framework_simplejwt.authentication import JWTAuthentication +import logging + + +from .models import Dingdan, DingdanPingtai, DingdanShangjia + + +logger = logging.getLogger(__name__) + +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 + + +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''' + + + +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 + + +# 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') + + +# ----------------- 基础配置获取 ----------------- +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): + """ + 代理发送群聊消息 + 支持: + - 非跨平台订单:直接用我方 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': '消息发送失败'})''' + + + + +'''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)''' + + + + + +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) + + + + + + + +from peizhi.models import ClubConfig, ShangjiaLianjie +logger = logging.getLogger('dingdan') + +# ---------- 跨平台对方配置获取 ---------- +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': '我方发送成功,对方发送失败'}) + + + +# 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 DashouCOSZhengshuView(APIView): + """打手获取COS上传凭证接口(使用腾讯云STS临时密钥)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + + + # 1. 接收参数 + dingdan_id = request.data.get('dingdan_id', '').strip() + print(dingdan_id) + 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)''' + +# + + +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 '', + 'dashou_liuyan': dingdan.dashou_liuyan or '', + 'fuwudashou_id': dingdan.jiedan_dashou_id or '', + 'fadanpingtai': dingdan.fadan_pingtai or '', + 'tuikuan_liyou': dingdan.tkly 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 '', + + '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 '', + #'tuikuan_liyou': dingdan.tkly 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 + + dashou_profile.zhuangtai = 1 + + # 更新打手扩展表数据 + # ✅ 确认字段: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}扩展信息不存在,跳过商家更新") + + # 11. 返回成功响应 + # 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)}") + + 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 AdQiangZhiJieDan(APIView): + """后台强制结单接口(支持跨平台订单)""" + 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. 身份验证 + dangqianyonghu = request.user + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + try: + guanliyuan = dangqianyonghu.admin_profile + except AttributeError: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 3. 查询订单并加锁(修复并发问题) + with transaction.atomic(): + try: + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response( + {'code': 404, 'message': '订单不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 4. 验证订单状态 + if order.zhuangtai not in [2, 8, 4]: + return Response( + {'code': 400, 'message': '订单状态不允许强制结单', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 获取订单字段 + jiedan_dashou_id = order.jiedan_dashou_id + dashou_fencheng = order.dashou_fencheng or Decimal('0') + fadan_pingtai = order.fadan_pingtai + dispatch_type = order.dispatch_type + is_cross = order.is_cross + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + jine = order.jine or Decimal('0') + + if dashou_fencheng is None: + return Response( + {'code': 400, 'message': '订单缺少打手分成金额', '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': qianzhanghao, + } + ) + 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: + # 处理打手结算 + 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.chengjiaozongliang = F('chengjiaozongliang') + 1 + dashou_profile.yue = F('yue') + dashou_fencheng + dashou_profile.zonge = F('zonge') + dashou_fencheng + dashou_profile.jinrishouyi = F('jinrishouyi') + dashou_fencheng + dashou_profile.jinyueshouyi = F('jinyueshouyi') + dashou_fencheng + dashou_profile.save(update_fields=[ + 'zhuangtai', 'chengjiaozongliang', 'yue', 'zonge', + 'jinrishouyi', 'jinyueshouyi' + ]) + logger.info(f"强制结单成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}") + except (UserMain.DoesNotExist, AttributeError): + logger.warning(f"强制结单:打手{jiedan_dashou_id}不存在,跳过打手结算") + + # 处理商家订单 + if fadan_pingtai == 2: + try: + shangjia_kuozhan = order.shangjia_kuozhan + shangjia_id = shangjia_kuozhan.shangjia_id + if shangjia_id: + shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_profile = shangjia_user.shop_profile + shangjia_profile.chengjiao = F('chengjiao') + 1 + shangjia_profile.save(update_fields=['chengjiao']) + logger.info(f"强制结单成功,商家{shangjia_id}成交订单+1") + except Exception as e: + logger.warning(f"强制结单:商家更新失败 - {str(e)}") + + order.zhuangtai = 3 + order.save() + + if fadan_pingtai == 2 and order.zhuangtai == 3: + from dingdan.utils import settle_shangjia_order_guanshi_fenhong + settle_shangjia_order_guanshi_fenhong(order, jiedan_dashou_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 + ) + + + +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(退款审核中) + + # 8. 验证订单状态是否为允许退款的状态 + allowed_statuses = [4, 8, 1, 2, 7] # 允许退款的状态:退款审核中、已下单、结算中、进行中、指定中 + if dingdan_obj.zhuangtai not in allowed_statuses: + return Response( + { + 'code': 400, + 'message': f'订单当前状态不允许退款。允许退款的状态为:退款审核中(4)、已下单(8)、结算中(1)、进行中(2)、指定中(7)', + 'data': None + }, + status=status.HTTP_400_BAD_REQUEST + ) + '''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.zhuangtai = 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) + # 12.更新退款统计 + + + + 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__) + + +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 + ) + + + + + + + + + + + + + + + +from peizhi.models import Club ,OrderTypeMapping + +logger = logging.getLogger(__name__) + +'''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)''' + + + + +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}") + 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) + + + + + + +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'}) + + 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 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 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 PartnerCheckClaimView(APIView): + """ + 接收对方平台抢单询问接口 + 对方平台打手抢单前,调用此接口询问订单是否可抢。 + 请求方式: POST + 接口地址: /dingdan/partner_check_claim + 请求参数: + token: 认证令牌 + partner_order_id: 对方平台订单ID(即我方记录的 partner_order_id) + dashou_id: 对方平台打手ID + 返回: + code: 0成功 + data: { + can_claim: true/false // true表示我方允许抢单,且已更新订单状态 + } + 注意:此接口会原子地检查并更新订单状态,必须使用行锁。 + """ + 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) + + partner_order_id = request.data.get('partner_order_id') + dashou_id = request.data.get('dashou_id') + + if not partner_order_id or not dashou_id: + logger.error(f"跨平台抢单询问参数缺失: {request.data}") + return Response({'code': 400, 'message': 'Missing parameters'}, status=status.HTTP_400_BAD_REQUEST) + + try: + with transaction.atomic(): + # 使用 select_for_update 锁定订单行,防止并发 + order = Dingdan.objects.select_for_update().get(dingdan_id=partner_order_id) + + # 检查订单状态是否为1(待抢) + if order.zhuangtai == 1: + # 允许抢单,更新订单 + order.zhuangtai = 2 + order.jiedan_dashou_id = dashou_id + order.is_cross = 1 # 标记为被跨平台抢单 + order.save() + logger.info(f"跨平台抢单成功: dingdan_id={partner_order_id}, 被对方打手 {dashou_id} 抢走") + return Response({'code': 0, 'data': {'can_claim': True}}) + else: + # 订单已被抢,不可抢 + logger.info(f"跨平台抢单失败: dingdan_id={partner_order_id}, 当前状态 {order.zhuangtai}") + return Response({'code': 0, 'data': {'can_claim': False}}) + 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) + 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) + + + + + + +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/__pycache__/apps.cpython-313.pyc b/houtai/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000..54fde51 Binary files /dev/null and b/houtai/__pycache__/apps.cpython-313.pyc differ diff --git a/houtai/__pycache__/models.cpython-313.pyc b/houtai/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..8b0ef0d Binary files /dev/null and b/houtai/__pycache__/models.cpython-313.pyc differ diff --git a/houtai/__pycache__/serializers.cpython-313.pyc b/houtai/__pycache__/serializers.cpython-313.pyc new file mode 100644 index 0000000..253ef14 Binary files /dev/null and b/houtai/__pycache__/serializers.cpython-313.pyc differ diff --git a/houtai/__pycache__/urls.cpython-313.pyc b/houtai/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..b27ea65 Binary files /dev/null and b/houtai/__pycache__/urls.cpython-313.pyc differ diff --git a/houtai/__pycache__/utils.cpython-312.pyc b/houtai/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..2fe48dd Binary files /dev/null and b/houtai/__pycache__/utils.cpython-312.pyc differ diff --git a/houtai/__pycache__/utils.cpython-313.pyc b/houtai/__pycache__/utils.cpython-313.pyc new file mode 100644 index 0000000..680213a Binary files /dev/null and b/houtai/__pycache__/utils.cpython-313.pyc differ diff --git a/houtai/__pycache__/view.cpython-313.pyc b/houtai/__pycache__/view.cpython-313.pyc new file mode 100644 index 0000000..4057088 Binary files /dev/null and b/houtai/__pycache__/view.cpython-313.pyc differ diff --git a/houtai/_init_.py b/houtai/_init_.py new file mode 100644 index 0000000..e69de29 diff --git a/houtai/apps.py b/houtai/apps.py new file mode 100644 index 0000000..bc21450 --- /dev/null +++ b/houtai/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class HoutaiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'houtai' \ No newline at end of file diff --git a/houtai/migrations/0001_initial.py b/houtai/migrations/0001_initial.py new file mode 100644 index 0000000..e71b4e2 --- /dev/null +++ b/houtai/migrations/0001_initial.py @@ -0,0 +1,296 @@ +# Generated by Django 4.2.27 on 2026-04-06 23:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="AbnormalUserLog", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "account_id", + models.CharField( + db_index=True, max_length=20, verbose_name="账号ID(phone)" + ), + ), + ( + "ip_address", + models.GenericIPAddressField( + blank=True, null=True, verbose_name="IP地址" + ), + ), + ( + "real_ip", + models.GenericIPAddressField( + blank=True, null=True, verbose_name="真实IP(穿透代理后)" + ), + ), + ( + "x_forwarded_for", + models.CharField( + blank=True, + default="", + max_length=255, + verbose_name="X-Forwarded-For", + ), + ), + ( + "user_agent", + models.CharField( + blank=True, + default="", + max_length=500, + verbose_name="User-Agent", + ), + ), + ( + "request_method", + models.CharField( + blank=True, default="", max_length=10, verbose_name="请求方法" + ), + ), + ( + "request_path", + models.CharField( + blank=True, default="", max_length=500, verbose_name="请求路径" + ), + ), + ( + "request_params", + models.TextField( + blank=True, default="", verbose_name="请求参数(GET/POST)" + ), + ), + ( + "request_body", + models.TextField( + blank=True, default="", verbose_name="请求体(原始)" + ), + ), + ( + "request_headers", + models.TextField( + blank=True, default="", verbose_name="请求头(JSON格式)" + ), + ), + ( + "attempt_type", + models.CharField( + blank=True, default="", max_length=50, verbose_name="攻击类型" + ), + ), + ( + "detail", + models.TextField(blank=True, default="", verbose_name="详细描述"), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ], + options={ + "verbose_name": "异常用户日志", + "verbose_name_plural": "异常用户日志", + "db_table": "abnormal_user_log", + }, + ), + migrations.CreateModel( + name="Permission", + fields=[ + ( + "perm_id", + models.AutoField( + primary_key=True, serialize=False, verbose_name="权限ID" + ), + ), + ( + "perm_code", + models.CharField( + db_index=True, + max_length=100, + unique=True, + verbose_name="权限编码", + ), + ), + ( + "perm_name", + models.CharField(max_length=100, verbose_name="权限名称"), + ), + ( + "description", + models.CharField( + blank=True, default="", max_length=200, verbose_name="权限描述" + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "verbose_name": "权限", + "verbose_name_plural": "权限", + "db_table": "permission", + }, + ), + migrations.CreateModel( + name="Role", + fields=[ + ( + "role_id", + models.AutoField( + primary_key=True, serialize=False, verbose_name="角色ID" + ), + ), + ( + "role_code", + models.CharField( + db_index=True, + max_length=50, + unique=True, + verbose_name="角色编码", + ), + ), + ("role_name", models.CharField(max_length=50, verbose_name="角色名称")), + ( + "description", + models.CharField( + blank=True, default="", max_length=200, verbose_name="角色描述" + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "verbose_name": "角色", + "verbose_name_plural": "角色", + "db_table": "role", + }, + ), + migrations.CreateModel( + name="UserRole", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "account_id", + models.CharField( + db_index=True, max_length=11, verbose_name="账号ID" + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ( + "role", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="houtai.role", + verbose_name="角色", + ), + ), + ], + options={ + "verbose_name": "用户角色关联", + "verbose_name_plural": "用户角色关联", + "db_table": "user_role", + }, + ), + migrations.CreateModel( + name="RolePermission", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "permission", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="houtai.permission", + verbose_name="权限", + ), + ), + ( + "role", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="houtai.role", + verbose_name="角色", + ), + ), + ], + options={ + "verbose_name": "角色权限关联", + "verbose_name_plural": "角色权限关联", + "db_table": "role_permission", + }, + ), + migrations.AddIndex( + model_name="role", + index=models.Index(fields=["role_code"], name="role_role_co_b9f497_idx"), + ), + migrations.AddIndex( + model_name="permission", + index=models.Index( + fields=["perm_code"], name="permission_perm_co_56cbe3_idx" + ), + ), + migrations.AddIndex( + model_name="abnormaluserlog", + index=models.Index( + fields=["account_id"], name="abnormal_us_account_425899_idx" + ), + ), + migrations.AddIndex( + model_name="abnormaluserlog", + index=models.Index( + fields=["ip_address"], name="abnormal_us_ip_addr_3d612b_idx" + ), + ), + migrations.AddIndex( + model_name="abnormaluserlog", + index=models.Index( + fields=["create_time"], name="abnormal_us_create__ee16da_idx" + ), + ), + migrations.AlterUniqueTogether( + name="userrole", + unique_together={("account_id", "role")}, + ), + migrations.AlterUniqueTogether( + name="rolepermission", + unique_together={("role", "permission")}, + ), + ] diff --git a/houtai/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py b/houtai/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py new file mode 100644 index 0000000..6284dd8 --- /dev/null +++ b/houtai/migrations/0002_zuzhangritongji_shangjiaritongji_and_more.py @@ -0,0 +1,339 @@ +# Generated by Django 4.2.27 on 2026-04-09 14:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("houtai", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="ZuzhangRiTongji", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "yonghuid", + models.CharField( + db_index=True, max_length=7, verbose_name="组长用户ID" + ), + ), + ("riqi", models.DateField(db_index=True, verbose_name="日期")), + ( + "yaoqing_guanshi_shu", + models.IntegerField(default=0, verbose_name="邀请管事总数"), + ), + ( + "shouru_zonge", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="收入总额(分佣)", + ), + ), + ( + "fenyong_jine", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="分佣金额", + ), + ), + ], + options={ + "verbose_name": "组长每日统计", + "db_table": "zuzhang_ri_tongji", + "indexes": [ + models.Index( + fields=["riqi", "-yaoqing_guanshi_shu"], + name="zuzhang_ri__riqi_87169e_idx", + ), + models.Index( + fields=["riqi", "-shouru_zonge"], + name="zuzhang_ri__riqi_1d7e51_idx", + ), + ], + "unique_together": {("yonghuid", "riqi")}, + }, + ), + migrations.CreateModel( + name="ShangjiaRiTongji", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "yonghuid", + models.CharField( + db_index=True, max_length=7, verbose_name="商家用户ID" + ), + ), + ("riqi", models.DateField(db_index=True, verbose_name="日期")), + ( + "paifa_dingdan_shu", + models.IntegerField(default=0, verbose_name="派发订单总数"), + ), + ( + "paifa_jine", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="派发订单总金额", + ), + ), + ( + "jiesuan_dingdan_shu", + models.IntegerField(default=0, verbose_name="结算订单总数"), + ), + ( + "jiesuan_jine", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="结算总金额", + ), + ), + ( + "tuikuan_dingdan_shu", + models.IntegerField(default=0, verbose_name="退款订单总数"), + ), + ( + "tuikuan_jine", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="退款总金额", + ), + ), + ], + options={ + "verbose_name": "商家每日统计", + "db_table": "shangjia_ri_tongji", + "indexes": [ + models.Index( + fields=["riqi", "-paifa_jine"], + name="shangjia_ri_riqi_d00f7f_idx", + ), + models.Index( + fields=["riqi", "-jiesuan_jine"], + name="shangjia_ri_riqi_25772a_idx", + ), + models.Index( + fields=["riqi", "-paifa_dingdan_shu"], + name="shangjia_ri_riqi_e05c54_idx", + ), + ], + "unique_together": {("yonghuid", "riqi")}, + }, + ), + migrations.CreateModel( + name="GuanshiXufeiRiTongji", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "yonghuid", + models.CharField( + db_index=True, max_length=7, verbose_name="管事用户ID" + ), + ), + ("riqi", models.DateField(db_index=True, verbose_name="日期")), + ( + "xufei_zongshu", + models.IntegerField(default=0, verbose_name="今日续费总数"), + ), + ( + "xufei_shouyi", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="续费收益金额", + ), + ), + ], + options={ + "verbose_name": "管事续费每日统计", + "db_table": "guanshi_xufei_ri_tongji", + "indexes": [ + models.Index( + fields=["riqi", "-xufei_zongshu"], + name="guanshi_xuf_riqi_7c128f_idx", + ), + models.Index( + fields=["riqi", "-xufei_shouyi"], + name="guanshi_xuf_riqi_8f09b5_idx", + ), + ], + "unique_together": {("yonghuid", "riqi")}, + }, + ), + migrations.CreateModel( + name="GuanshiRiTongji", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "yonghuid", + models.CharField( + db_index=True, max_length=7, verbose_name="管事用户ID" + ), + ), + ("riqi", models.DateField(db_index=True, verbose_name="日期")), + ( + "yaoqing_dashou_shu", + models.IntegerField(default=0, verbose_name="邀请打手总数"), + ), + ( + "chongzhi_dashou_shu", + models.IntegerField(default=0, verbose_name="充值打手数量"), + ), + ( + "shouru_zonge", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="收入总额", + ), + ), + ], + options={ + "verbose_name": "管事每日统计", + "db_table": "guanshi_ri_tongji", + "indexes": [ + models.Index( + fields=["riqi", "-yaoqing_dashou_shu"], + name="guanshi_ri__riqi_075e1a_idx", + ), + models.Index( + fields=["riqi", "-chongzhi_dashou_shu"], + name="guanshi_ri__riqi_78a79d_idx", + ), + models.Index( + fields=["riqi", "-shouru_zonge"], + name="guanshi_ri__riqi_334f52_idx", + ), + ], + "unique_together": {("yonghuid", "riqi")}, + }, + ), + migrations.CreateModel( + name="DashouRiTongji", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "yonghuid", + models.CharField( + db_index=True, max_length=7, verbose_name="打手用户ID" + ), + ), + ("riqi", models.DateField(db_index=True, verbose_name="日期")), + ( + "jiedan_zongliang", + models.IntegerField(default=0, verbose_name="接单总量"), + ), + ( + "chengjiao_zongliang", + models.IntegerField(default=0, verbose_name="成交单总量"), + ), + ( + "jiedan_zonge", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="接单总金额", + ), + ), + ( + "chengjiao_zonge", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="成交总金额", + ), + ), + ( + "tuikuan_liang", + models.IntegerField(default=0, verbose_name="退款订单量"), + ), + ( + "tuikuan_jine", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="退款总金额", + ), + ), + ], + options={ + "verbose_name": "打手每日统计", + "db_table": "dashou_ri_tongji", + "indexes": [ + models.Index( + fields=["riqi", "-jiedan_zongliang"], + name="dashou_ri_t_riqi_95a785_idx", + ), + models.Index( + fields=["riqi", "-chengjiao_zongliang"], + name="dashou_ri_t_riqi_ad9c1a_idx", + ), + models.Index( + fields=["riqi", "-jiedan_zonge"], + name="dashou_ri_t_riqi_5da703_idx", + ), + models.Index( + fields=["riqi", "-chengjiao_zonge"], + name="dashou_ri_t_riqi_0a3156_idx", + ), + ], + "unique_together": {("yonghuid", "riqi")}, + }, + ), + ] diff --git a/houtai/migrations/0003_tixianritongji.py b/houtai/migrations/0003_tixianritongji.py new file mode 100644 index 0000000..8149a08 --- /dev/null +++ b/houtai/migrations/0003_tixianritongji.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.27 on 2026-04-10 18:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("houtai", "0002_zuzhangritongji_shangjiaritongji_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="TixianRiTongji", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("riqi", models.DateField(db_index=True, verbose_name="日期")), + ( + "leixing", + models.IntegerField( + choices=[(1, "打手"), (2, "管事"), (3, "组长")], + verbose_name="用户类型", + ), + ), + ( + "total_amount", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=12, + verbose_name="当天提现总额(元)", + ), + ), + ], + options={ + "verbose_name": "每日提现统计", + "db_table": "tixian_ri_tongji", + "indexes": [ + models.Index( + fields=["riqi", "leixing"], name="tixian_ri_t_riqi_c18bb2_idx" + ) + ], + "unique_together": {("riqi", "leixing")}, + }, + ), + ] diff --git a/houtai/migrations/0024_alter_tixianritongji_leixing.py b/houtai/migrations/0024_alter_tixianritongji_leixing.py new file mode 100644 index 0000000..3e70ca4 --- /dev/null +++ b/houtai/migrations/0024_alter_tixianritongji_leixing.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.27 on 2026-06-12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("houtai", "0003_tixianritongji"), + ] + + operations = [ + migrations.AlterField( + model_name="tixianritongji", + name="leixing", + field=models.IntegerField( + choices=[ + (1, "打手"), + (2, "管事"), + (3, "组长"), + (4, "考核官"), + (5, "打手押金"), + (6, "商家余额"), + ], + verbose_name="用户类型", + ), + ), + ] diff --git a/houtai/migrations/__init__.py b/houtai/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/houtai/migrations/__pycache__/0001_initial.cpython-313.pyc b/houtai/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000..e47cd67 Binary files /dev/null and b/houtai/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/houtai/migrations/__pycache__/0002_zuzhangritongji_shangjiaritongji_and_more.cpython-313.pyc b/houtai/migrations/__pycache__/0002_zuzhangritongji_shangjiaritongji_and_more.cpython-313.pyc new file mode 100644 index 0000000..a4c3b29 Binary files /dev/null and b/houtai/migrations/__pycache__/0002_zuzhangritongji_shangjiaritongji_and_more.cpython-313.pyc differ diff --git a/houtai/migrations/__pycache__/0003_tixianritongji.cpython-313.pyc b/houtai/migrations/__pycache__/0003_tixianritongji.cpython-313.pyc new file mode 100644 index 0000000..5983468 Binary files /dev/null and b/houtai/migrations/__pycache__/0003_tixianritongji.cpython-313.pyc differ diff --git a/houtai/migrations/__pycache__/__init__.cpython-313.pyc b/houtai/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..7ecec24 Binary files /dev/null and b/houtai/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/houtai/migrations/_init_.py b/houtai/migrations/_init_.py new file mode 100644 index 0000000..e69de29 diff --git a/houtai/models.py b/houtai/models.py new file mode 100644 index 0000000..c97883e --- /dev/null +++ b/houtai/models.py @@ -0,0 +1,272 @@ +from django.db import models + +# ==================== 1. 角色表 ==================== +class Role(models.Model): + """角色表""" + role_id = models.AutoField(primary_key=True, verbose_name='角色ID') + role_code = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='角色编码') + role_name = models.CharField(max_length=50, verbose_name='角色名称') + description = models.CharField(max_length=200, blank=True, default='', verbose_name='角色描述') + + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'role' + verbose_name = '角色' + verbose_name_plural = '角色' + indexes = [ + models.Index(fields=['role_code']), + + ] + + def __str__(self): + return self.role_name + + +# ==================== 2. 权限表 ==================== +class Permission(models.Model): + """权限表""" + perm_id = models.AutoField(primary_key=True, verbose_name='权限ID') + perm_code = models.CharField(max_length=100, unique=True, db_index=True, verbose_name='权限编码') + perm_name = models.CharField(max_length=100, verbose_name='权限名称') + description = models.CharField(max_length=200, blank=True, default='', verbose_name='权限描述') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'permission' + verbose_name = '权限' + verbose_name_plural = '权限' + indexes = [ + models.Index(fields=['perm_code']), + ] + + def __str__(self): + return f"{self.perm_name} ({self.perm_code})" + + +# ==================== 3. 角色-权限关联表 ==================== +class RolePermission(models.Model): + """角色权限关联表""" + id = models.AutoField(primary_key=True) + role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色') + permission = models.ForeignKey(Permission, on_delete=models.CASCADE, db_index=True, verbose_name='权限') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'role_permission' + verbose_name = '角色权限关联' + verbose_name_plural = '角色权限关联' + unique_together = [['role', 'permission']] # 防止重复授权 + + def __str__(self): + return f"{self.role.role_name} -> {self.permission.perm_name}" + + +# ==================== 4. 用户-角色关联表 ==================== +class UserRole(models.Model): + """用户角色关联表""" + id = models.AutoField(primary_key=True) + account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID') # 对应 UserMain 的 phone 字段 + role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色') + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'user_role' + verbose_name = '用户角色关联' + verbose_name_plural = '用户角色关联' + unique_together = [['account_id', 'role']] # 同一用户同一角色只能有一条 + + def __str__(self): + return f"{self.account_id} -> {self.role.role_name}" + + +class AbnormalUserLog(models.Model): + """记录恶意尝试越权操作的用户信息""" + # 账号信息 + account_id = models.CharField(max_length=20, db_index=True, verbose_name='账号ID(phone)') + + # 网络信息 + ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP地址') + real_ip = models.GenericIPAddressField(null=True, blank=True, verbose_name='真实IP(穿透代理后)') + x_forwarded_for = models.CharField(max_length=255, blank=True, default='', verbose_name='X-Forwarded-For') + user_agent = models.CharField(max_length=500, blank=True, default='', verbose_name='User-Agent') + + # 地理位置信息(需借助第三方API,这里只存储请求时的IP,后续可离线解析) + # 但我们可以存储更多请求细节 + request_method = models.CharField(max_length=10, blank=True, default='', verbose_name='请求方法') + request_path = models.CharField(max_length=500, blank=True, default='', verbose_name='请求路径') + request_params = models.TextField(blank=True, default='', verbose_name='请求参数(GET/POST)') + request_body = models.TextField(blank=True, default='', verbose_name='请求体(原始)') + request_headers = models.TextField(blank=True, default='', verbose_name='请求头(JSON格式)') + + # 攻击特征 + attempt_type = models.CharField(max_length=50, blank=True, default='', verbose_name='攻击类型') + detail = models.TextField(blank=True, default='', verbose_name='详细描述') + + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'abnormal_user_log' + verbose_name = '异常用户日志' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['account_id']), + models.Index(fields=['ip_address']), + models.Index(fields=['create_time']), + ] + + def __str__(self): + return f"{self.account_id} @ {self.ip_address} - {self.create_time}" + + + + + +class ShangjiaRiTongji(models.Model): + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='商家用户ID') + riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 + + # 派发统计 + paifa_dingdan_shu = models.IntegerField(default=0, verbose_name='派发订单总数') + paifa_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='派发订单总金额') + + # 结算统计(成交) + jiesuan_dingdan_shu = models.IntegerField(default=0, verbose_name='结算订单总数') + jiesuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='结算总金额') + + # 退款统计(可选) + tuikuan_dingdan_shu = models.IntegerField(default=0, verbose_name='退款订单总数') + tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额') + + class Meta: + db_table = 'shangjia_ri_tongji' + verbose_name = '商家每日统计' + unique_together = [['yonghuid', 'riqi']] # 每人每天一条记录 + indexes = [ + models.Index(fields=['riqi', '-paifa_jine']), + models.Index(fields=['riqi', '-jiesuan_jine']), + models.Index(fields=['riqi', '-paifa_dingdan_shu']), + ] + + + + + +class DashouRiTongji(models.Model): + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='打手用户ID') + riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 + + # 接单统计 + jiedan_zongliang = models.IntegerField(default=0, verbose_name='接单总量') + chengjiao_zongliang = models.IntegerField(default=0, verbose_name='成交单总量') + + # 金额统计 + jiedan_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='接单总金额') + chengjiao_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='成交总金额') + + # 退款统计 + tuikuan_liang = models.IntegerField(default=0, verbose_name='退款订单量') + tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额') + + class Meta: + db_table = 'dashou_ri_tongji' + verbose_name = '打手每日统计' + unique_together = [['yonghuid', 'riqi']] + indexes = [ + models.Index(fields=['riqi', '-jiedan_zongliang']), + models.Index(fields=['riqi', '-chengjiao_zongliang']), + models.Index(fields=['riqi', '-jiedan_zonge']), + models.Index(fields=['riqi', '-chengjiao_zonge']), + ] + + + +class ZuzhangRiTongji(models.Model): + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='组长用户ID') + riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 + + yaoqing_guanshi_shu = models.IntegerField(default=0, verbose_name='邀请管事总数') + shouru_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额(分佣)') + + # 可选明细 + fenyong_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='分佣金额') + + class Meta: + db_table = 'zuzhang_ri_tongji' + verbose_name = '组长每日统计' + unique_together = [['yonghuid', 'riqi']] + indexes = [ + models.Index(fields=['riqi', '-yaoqing_guanshi_shu']), + models.Index(fields=['riqi', '-shouru_zonge']), + ] + + + + +class GuanshiRiTongji(models.Model): + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID') + riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 + + yaoqing_dashou_shu = models.IntegerField(default=0, verbose_name='邀请打手总数') + chongzhi_dashou_shu = models.IntegerField(default=0, verbose_name='充值打手数量') + shouru_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额') + + class Meta: + db_table = 'guanshi_ri_tongji' + verbose_name = '管事每日统计' + unique_together = [['yonghuid', 'riqi']] + indexes = [ + models.Index(fields=['riqi', '-yaoqing_dashou_shu']), + models.Index(fields=['riqi', '-chongzhi_dashou_shu']), + models.Index(fields=['riqi', '-shouru_zonge']), + ] + + + + +class GuanshiXufeiRiTongji(models.Model): + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID') + riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 + + xufei_zongshu = models.IntegerField(default=0, verbose_name='今日续费总数') # 续费订单数(或续费打手数) + xufei_shouyi = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='续费收益金额') + + class Meta: + db_table = 'guanshi_xufei_ri_tongji' + verbose_name = '管事续费每日统计' + unique_together = [['yonghuid', 'riqi']] + indexes = [ + models.Index(fields=['riqi', '-xufei_zongshu']), + models.Index(fields=['riqi', '-xufei_shouyi']), + ] + + + +# models.py 中添加 + +class TixianRiTongji(models.Model): + """ + 每日提现统计表(按身份统计当天提现总额) + """ + riqi = models.DateField(db_index=True, verbose_name='日期') + leixing = models.IntegerField( + choices=[ + (1, '打手'), (2, '管事'), (3, '组长'), + (4, '考核官'), (5, '打手押金'), (6, '商家余额'), + ], + verbose_name='用户类型' + ) + total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='当天提现总额(元)') + + class Meta: + db_table = 'tixian_ri_tongji' + verbose_name = '每日提现统计' + unique_together = [['riqi', 'leixing']] # 每天每个身份一条记录 + indexes = [models.Index(fields=['riqi', 'leixing'])] + + def __str__(self): + return f"{self.riqi} - {self.get_leixing_display()} 提现总额: {self.total_amount}" \ No newline at end of file diff --git a/houtai/serializers.py b/houtai/serializers.py new file mode 100644 index 0000000..68c7b37 --- /dev/null +++ b/houtai/serializers.py @@ -0,0 +1,39 @@ +# serializers.py + +#小程序弹窗序列化器 +from rest_framework import serializers +from peizhi.models import PopupPage, PopupConfig, PopupImage + +class PopupImageSerializer(serializers.ModelSerializer): + """图片序列化器(输出用)""" + class Meta: + model = PopupImage + fields = ['id', 'image_url', 'text', 'sort_order'] + + +class PopupConfigSerializer(serializers.ModelSerializer): + """弹窗配置序列化器(输出用)""" + images = PopupImageSerializer(many=True, read_only=True) + + class Meta: + model = PopupConfig + fields = [ + 'id', 'popup_id', 'title', 'description', + 'strategy_type', 'max_count', 'reset_interval', + 'duration', 'force_even_muted', 'sort_order', + 'is_active', 'start_time', 'end_time', + 'created_at', 'updated_at', 'images' + ] + + +class PopupPageSerializer(serializers.ModelSerializer): + """页面序列化器(输出用)""" + popups = PopupConfigSerializer(many=True, read_only=True) + + class Meta: + model = PopupPage + fields = [ + 'id', 'page_key', 'name', 'description', + 'is_active', 'ignore_user_mute', + 'created_at', 'updated_at', 'popups' + ] \ No newline at end of file diff --git a/houtai/urls.py b/houtai/urls.py new file mode 100644 index 0000000..6301dde --- /dev/null +++ b/houtai/urls.py @@ -0,0 +1,134 @@ +from django.urls import path +from django.views.decorators.csrf import csrf_exempt + +from .view import GetClubConfigView, GetCrossOrderListView, PartnerGetOrderDataView, PartnerRefundNotifyView, \ + CrossPlatformRefundView, GetCrossOrderDetailView, CrossPlatformForceCompleteView, CrossPlatformTransferHallView, \ + PartnerTransferHallNotifyView, GetSettingsView, UpdateSettingsView, GetMappingsView, UpdateMappingsView, \ + PartnerUpdateSelfConfigView, GetPartnerClubsView, GetStatsView, PunishDashouView, PartnerPunishNotifyView, \ + GetClubConfigForPreSettlementView, PreSettlementActionView, GetPreSettlementDataView, GetRolePermissionView, \ + ModifyRolePermissionView, AddRoleView, GetAdminRolesView, GetAdminUserListView, AddAdminUserView, \ + ModifyAdminUserView, KefuGetDashouListView, KefuGetDashouDetailView, KefuUpdateDashouView, KefuGetShangjiaListView, \ + KefuGetShangjiaDetailView, KefuUpdateShangjiaView, GetProductBaseDataView, GetProductListView, SaveProductOrderView, \ + AddProductView, DeleteProductView, GetProductDetailView, UpdateProductView, UpdateMemberView, GetMemberListView, \ + AddMemberView, GetGuanliListView, GetGuanliDetailView, UpdateGuanliView, GetWithdrawSettingsView, \ + UpdateWithdrawSettingsView, GetZuzhangListView, GetZuzhangDetailView, UpdateZuzhangView, GetProductTypeZoneView, \ + ModifyProductTypeZoneView, GetRateView, ModifyRateView, PopupNoticeListAPIView, PopupNoticeModifyAPIView, \ + ShopListView, ShopModifyView, ShopPublicTypeAndAuditView, ShopListForProductView, ShopProductModifyView, \ + ShopProductTypeMappingView, ProductListView, ProductModifyView, KefuChatPermissionsView, PartnerFineNotifyView, \ + FineApplyView, FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView, HqbkxxView, BkxgView, \ + CaiwuView, CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView, CwqtczhqView, KhpzhqView, ChzsgcView, \ + KhgglView, ShgxgsjView, ZxkfghdsView, KptxwztjbView + +urlpatterns = [ + + path('kpthq', GetClubConfigView.as_view(), name='跨平台订单页面获取俱乐部数据'), + path('hqddlb', GetCrossOrderListView.as_view(), name='获取跨平台订单'), + path('kpthqtp',csrf_exempt(PartnerGetOrderDataView.as_view()), name='跨平台接受对方服务器获取订单数据'), + # 对方平台退款通知接口 + path('dfddtk', csrf_exempt(PartnerRefundNotifyView.as_view()), name='跨平台接受对方服务器更新订单状态'), + path('kptk', CrossPlatformRefundView.as_view(), name='跨平台订单退款'), + path('hqkptddxq', GetCrossOrderDetailView.as_view(), name='获取跨平台订单数据详情'), + path('kpqzjd', CrossPlatformForceCompleteView.as_view(), name='跨平台订单结算'), + path('kpzydt', CrossPlatformTransferHallView.as_view(), name='跨平台转移大厅'), + path('kpttzzydt', csrf_exempt(PartnerTransferHallNotifyView.as_view()), name='跨平台接受对方转移大厅通知'), + path('hqkptsz', GetSettingsView.as_view(), name='获取俱乐部数据信息'), + path('xgjlbsj', UpdateSettingsView.as_view(), name='更新俱乐部数据信息'), + path('hqqtjlbsj', GetMappingsView.as_view(), name='获取俱乐部订单类型映射'), + path('xgdfddys', UpdateMappingsView.as_view(), name='工薪俱乐部订单类型映射'), + path('xgzjsjcs', csrf_exempt(PartnerUpdateSelfConfigView.as_view()), name='partner_update_self_config'), + path('kpthqpt', GetPartnerClubsView.as_view(), name='前端获取俱乐部数据展示派发数据'), + path('hqpfsj', GetStatsView.as_view(), name='前端派发统计表获取'), + path('kpcf', PunishDashouView.as_view(), name='跨平台处罚打手'), + path('kptcfds', PartnerPunishNotifyView.as_view(), name='接受跨平台对方平台处罚申请'), + path('hqkttjsyjs', GetClubConfigForPreSettlementView.as_view(), name='预处理获取俱乐部数据'), + path('hqyjssj', GetPreSettlementDataView.as_view(), name='获取预处理数据'), + path('yjsjjhdk', PreSettlementActionView.as_view(), name='操作预处理'), + + path('gljshq', GetRolePermissionView.as_view(), name='获取就角色信息'), + path('sctjjsnr', ModifyRolePermissionView.as_view(), name='修改角色数据'), + path('tjjs', AddRoleView.as_view(), name='添加角色'), + + path('hqyhjsgl', GetAdminRolesView.as_view(), name='管理用户页面获取用户角色'), + path('hqjsyhsj', GetAdminUserListView.as_view(), name='管理用户页面获取用户数据'), + path('xghtyhsj', ModifyAdminUserView.as_view(), name='管理用户页面修改用户数据'), + path('tjyhjs', AddAdminUserView.as_view(), name='管理用户页面添加用户'), + path('kefuhqdslb', KefuGetDashouListView.as_view(), name='客服获取打手数据'), + path('kefuhqdsxq', KefuGetDashouDetailView.as_view(), name='客服获取打手具体信息'), + path('kefuxgds', KefuUpdateDashouView.as_view(), name='客服修改打手数据'), + path('hqsjgllb', KefuGetShangjiaListView.as_view(), name='后台获取商家列表'), + path('hthqsjxq', KefuGetShangjiaDetailView.as_view(), name='后台获取商家详情'), + path('xgsjxx', KefuUpdateShangjiaView.as_view(), name='后台修改商家数据'), + + path('htsphqjcsx', GetProductBaseDataView.as_view(), name='后台获取商品类型专区'), + path('hthqsplb', GetProductListView.as_view(), name='后台获取商品数据'), + path('htsppaixu', SaveProductOrderView.as_view(), name='后台修改商品类型排序'), + path('htfbsp', AddProductView.as_view(), name='后台添加商品'), + path('htscsp', DeleteProductView.as_view(), name='后台删除商品'), + path('htsphqxq', GetProductDetailView.as_view(), name='后台获取商品详情'), + path('bcspxg', UpdateProductView.as_view(), name='后台更新商品数据'), + + path('hthqhylb', GetMemberListView.as_view(), name='后台获取会员列表'), + path('htxghyxx', UpdateMemberView.as_view(), name='后台更新会员信息'), + path('httjhy', AddMemberView.as_view(), name='后台添加会员'), + path('hthqgslb', GetGuanliListView.as_view(), name='后台获取管事列表'), + + path('hthqgsxq', GetGuanliDetailView.as_view(), name='后台获取管事详情信息'), + path('htxggsxx', UpdateGuanliView.as_view(), name='后台更新管事数据'), + path('hthqtxpz', GetWithdrawSettingsView.as_view(), name='后台获取提现配置'), + path('htxgtxsz', UpdateWithdrawSettingsView.as_view(), name='后台修改提现配置'), + path('hthqzzlb', GetZuzhangListView.as_view(), name='后台获取组长列表'), + path('hthqzzxq', GetZuzhangDetailView.as_view(), name='后台获取组长详情页面'), + path('htxgzzxx', UpdateZuzhangView.as_view(), name='后台修改组长信息'), + + + path('hthqsplxzq', GetProductTypeZoneView.as_view(), name='后台获取商品类型专区'), + path('htxgsplx', ModifyProductTypeZoneView.as_view(), name='后台修改添加商品类型专区'), + path('hthqfhpz', GetRateView.as_view(), name='后台获取费率'), + path('htxgfl', ModifyRateView.as_view(), name='后台修改费率'), + path('hthqtcxx', PopupNoticeListAPIView.as_view(), name='后台获取弹窗配置'), + path('htxgtcxx', PopupNoticeModifyAPIView.as_view(), name='后台更新创建弹窗配置'), + + path('hqdplbsj', ShopListView.as_view(), name='获取店铺列表数据'), + path('xgdpxx', ShopModifyView.as_view(), name='修改或添加店铺数据'), + + + path('htdphqsplx', ShopPublicTypeAndAuditView.as_view(), name='后台获取店铺公共商品类型,默认审核配置'), + path('htdphqdpys', ShopListForProductView.as_view(), name='后台获取商品店铺'), + path('htdphqyssplx', ShopProductTypeMappingView.as_view(), name='后台获取商品店铺类型'), + path('htxgdpsplx', ShopProductModifyView.as_view(), name='后台修改店铺商品类型和默认审核模式'), + path('hqhtdpsplx', ProductListView.as_view(), name='后台获取店铺商品数据'), + path('htxgdpspsj', ProductModifyView.as_view(), name='后台修改店铺商品数据'), + + path('kfhqltqx', KefuChatPermissionsView.as_view(), name='后台获取客服聊天配置'), + + path('kffkdssq', FineApplyView.as_view(), name='跨平台处罚罚款申请接口'), + path('kffkdssq_notify', PartnerFineNotifyView.as_view(), name='跨屏要对方平台处罚通知接口'), + + path('htglyhqcfsltj', FaKuanTongJiView.as_view(),name='后台罚单统计图片'), # 罚款统计 + path('hthqfklb', FaKuanLieBiaoView.as_view(),name='后台罚单列表'), # 罚款列表 + path('glyclfk', FaKuanChuLiView.as_view(),name='后台罚单操作'), # 处理罚款 + path('htfksc', FaKuanChuangJianView.as_view(),name='后台生成罚单'), # 创建罚款 + + path('hqbkxx', HqbkxxView.as_view(), name='获取板块数据'), # 获取板块数据 + path('bkxg', BkxgView.as_view(), name='板块增删改查'), # 板块增删改 + + path('caiwu', CaiwuView.as_view(), name='财务页面基础数据'), + + path('cwhybkhq', CwhybkhqView.as_view(), name='财务会员模块'), + path('hybkjtsj', HybkjtsjView.as_view(), name='财务会员统计'), + path('szxx', SzxxView.as_view(), name='详细收支记录'), + path('cwddhqlx', CwddhqlxView.as_view(), name='后台财务获取订单类型'), + path('cwhqjtddsj', CwhqjtddsjView.as_view(), name='后台财务获取订单数据'), +# urls.py + path('cwqtczhq', CwqtczhqView.as_view(), name='财务获取其他充值统计'), + + path('khpzhq', KhpzhqView.as_view(), name='后台获取称号配置'), + path('chzsgc', ChzsgcView.as_view(), name='后台对称号增删改查'), + path('khggl', KhgglView.as_view(), name='获取考核管信息'), + path('shgxgsj', ShgxgsjView.as_view(), name='修改考核管数据'), + path('zxkfghds', ZxkfghdsView.as_view(), name='链接换打手或重新下单'), + + path('kptxwztjb', csrf_exempt(KptxwztjbView.as_view()), name='跨平台脚本询问订单状态'), + + +] \ No newline at end of file diff --git a/houtai/utils.py b/houtai/utils.py new file mode 100644 index 0000000..f2a0c69 --- /dev/null +++ b/houtai/utils.py @@ -0,0 +1,436 @@ +# houtai/utils.py +import logging +import json +from django.core.exceptions import ObjectDoesNotExist +from rest_framework.response import Response +from .models import AbnormalUserLog, UserRole, RolePermission, Permission, DashouRiTongji, ShangjiaRiTongji, \ + GuanshiXufeiRiTongji, GuanshiRiTongji, ZuzhangRiTongji, TixianRiTongji + +from django.db import transaction +from django.db.models import F +from datetime import date +from decimal import Decimal + +logger = logging.getLogger(__name__) + + +def _get_real_ip(request): + """获取真实IP(考虑代理)""" + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0].strip() + return ip, x_forwarded_for + else: + ip = request.META.get('REMOTE_ADDR') + return ip, '' + + +def _record_abnormal(account_id, request, attempt_type, detail=''): + """记录异常操作(仅用于越权等攻击行为)""" + try: + real_ip, xff = _get_real_ip(request) + headers = {k: v for k, v in request.META.items() if k.startswith('HTTP_')} + headers_json = json.dumps(headers, ensure_ascii=False)[:2000] + + request_params = json.dumps(request.query_params.dict(), ensure_ascii=False) if request.query_params else '' + request_body = request.body.decode('utf-8', errors='ignore')[:5000] + + AbnormalUserLog.objects.create( + account_id=account_id, + ip_address=real_ip, + real_ip=real_ip, + x_forwarded_for=xff, + user_agent=request.META.get('HTTP_USER_AGENT', ''), + request_method=request.method, + request_path=request.path, + request_params=request_params, + request_body=request_body, + request_headers=headers_json, + attempt_type=attempt_type, + detail=detail, + ) + logger.warning(f"记录异常用户: {account_id} IP:{real_ip} 类型:{attempt_type}") + except Exception as e: + logger.error(f"记录异常日志失败: {e}") + + +def verify_kefu_permission(request, username_from_frontend=None): + """ + 验证后台管理员身份,并返回该用户的所有权限列表(基于新RBAC表) + 只有前端传递的username与当前登录用户不匹配时,才记录异常并警告前端。 + + 参数: + request: DRF request 对象 + username_from_frontend: 前端传递的账号(用于防越权) + + 返回: + (kefu_obj, permissions_list) # 验证通过 + (None, Response) # 验证失败,直接返回Response + """ + current_user = request.user + + # ---------- 1. 必须是客服 ---------- + if current_user.user_type != 'kefu': + return None, Response({'code': 403, 'msg': '身份错误,非客服账号'}, status=403) + + # ---------- 2. 客服扩展表存在且状态正常 ---------- + try: + kefu = current_user.kefu_profile + if kefu.zhuangtai != 1: + return None, Response({'code': 403, 'msg': '客服账号已被禁用'}, status=403) + except ObjectDoesNotExist: + return None, Response({'code': 403, 'msg': '客服账号不存在'}, status=403) + + # ---------- 3. 越权检测(只记录前端篡改username的情况) ---------- + if username_from_frontend and current_user.phone != username_from_frontend: + detail = f'前端username: {username_from_frontend},实际登录账号: {current_user.phone}' + _record_abnormal(current_user.phone, request, '越权尝试篡改username', detail=detail) + return None, Response({ + 'code': 403, + 'msg': '身份错误,无权操作其他账号,相关操作已记录' + }, status=403) + + # ---------- 4. 获取用户的所有权限(通过角色) ---------- + # 步骤1:获取用户的所有角色ID + role_ids = UserRole.objects.filter(account_id=current_user.phone).values_list('role_id', flat=True) + # 步骤2:通过角色ID获取权限编码 + permissions = list(Permission.objects.filter( + rolepermission__role_id__in=role_ids + ).values_list('perm_code', flat=True).distinct()) + + if not permissions: + logger.warning(f"用户 {current_user.phone} 没有任何权限,请检查角色分配") + + return kefu, permissions + + + + + + +def update_dashou_daily_by_action(yonghuid, amount, action): + """ + 根据行为类型更新打手每日统计(接单、成交、退款) + + 参数: + yonghuid: 打手用户ID + amount: 本单涉及的金额(Decimal) + action: 1 = 接单, 2 = 成交(结算), 3 = 退款 + """ + amount = Decimal(str(amount)) if amount else Decimal('0.00') + if amount <= 0: + return # 金额非正不统计 + + today = date.today() + + with transaction.atomic(): + stat, created = DashouRiTongji.objects.select_for_update().get_or_create( + yonghuid=yonghuid, + riqi=today, + defaults={ + 'yonghuid': yonghuid, + 'riqi': today, + 'jiedan_zongliang': 0, + 'jiedan_zonge': Decimal('0.00'), + 'chengjiao_zongliang': 0, + 'chengjiao_zonge': Decimal('0.00'), + 'tuikuan_liang': 0, + 'tuikuan_jine': Decimal('0.00'), + } + ) + + update_fields = {} + if action == 1: # 接单 + update_fields['jiedan_zongliang'] = F('jiedan_zongliang') + 1 + update_fields['jiedan_zonge'] = F('jiedan_zonge') + amount + elif action == 2: # 成交 + update_fields['chengjiao_zongliang'] = F('chengjiao_zongliang') + 1 + update_fields['chengjiao_zonge'] = F('chengjiao_zonge') + amount + elif action == 3: # 退款 + update_fields['tuikuan_liang'] = F('tuikuan_liang') + 1 + update_fields['tuikuan_jine'] = F('tuikuan_jine') + amount + else: + return # 无效行为 + + if update_fields: + DashouRiTongji.objects.filter(id=stat.id).update(**update_fields) + +def update_shangjia_daily(yonghuid, amount, action): + """ + 根据操作行为更新商家每日统计(派发、结算、退款) + + 参数: + yonghuid: 商家用户ID + amount: 当前订单涉及的金额(Decimal) + action: 操作类型,1 = 派发,2 = 结算(成交),3 = 退款 + + 说明: + - 金额必须大于0,否则不执行任何操作 + - 使用 select_for_update + F() 表达式保证原子性和并发安全 + - 每天每个商家只会有一条统计记录,不存在则自动创建 + """ + amount = Decimal(str(amount)) if amount else Decimal('0.00') + if amount <= 0: + return # 金额非正,不统计 + + today = date.today() + + with transaction.atomic(): + # 获取或创建当天统计记录,同时加锁防止并发 + stat, created = ShangjiaRiTongji.objects.select_for_update().get_or_create( + yonghuid=yonghuid, + riqi=today, + defaults={ + 'yonghuid': yonghuid, + 'riqi': today, + 'paifa_dingdan_shu': 0, + 'paifa_jine': Decimal('0.00'), + 'jiesuan_dingdan_shu': 0, + 'jiesuan_jine': Decimal('0.00'), + 'tuikuan_dingdan_shu': 0, + 'tuikuan_jine': Decimal('0.00'), + } + ) + + # 构建本次更新的字段,使用 F 表达式进行原子累加 + update_fields = {} + if action == 1: # 派发 + update_fields['paifa_dingdan_shu'] = F('paifa_dingdan_shu') + 1 + update_fields['paifa_jine'] = F('paifa_jine') + amount + elif action == 2: # 结算(成交) + update_fields['jiesuan_dingdan_shu'] = F('jiesuan_dingdan_shu') + 1 + update_fields['jiesuan_jine'] = F('jiesuan_jine') + amount + elif action == 3: # 退款 + update_fields['tuikuan_dingdan_shu'] = F('tuikuan_dingdan_shu') + 1 + update_fields['tuikuan_jine'] = F('tuikuan_jine') + amount + else: + # 无效的行为类型,直接返回 + return + + # 执行原子更新 + ShangjiaRiTongji.objects.filter(id=stat.id).update(**update_fields) + +def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')): + """ + 根据操作行为更新管事每日统计(邀请打手、充值会员、订单/押金分红) + action: 1 = 邀请打手, 2 = 充值会员, 3 = 商家订单分红, 4 = 押金分红 + """ + from django.db import transaction + from django.db.models import F + from datetime import date + + today = date.today() + + with transaction.atomic(): + stat, created = GuanshiRiTongji.objects.select_for_update().get_or_create( + yonghuid=yonghuid, + riqi=today, + defaults={ + 'yonghuid': yonghuid, + 'riqi': today, + 'yaoqing_dashou_shu': 0, + 'chongzhi_dashou_shu': 0, + 'shouru_zonge': Decimal('0.00'), + } + ) + + if action == 1: + # 邀请打手:无论新创建还是已存在,直接 +1 + if created: + stat.yaoqing_dashou_shu = 1 + stat.save() + else: + GuanshiRiTongji.objects.filter(id=stat.id).update( + yaoqing_dashou_shu=F('yaoqing_dashou_shu') + 1 + ) + + elif action == 2: + amount = Decimal(str(amount)) if amount else Decimal('0.00') + if amount <= 0: + return + if created: + stat.chongzhi_dashou_shu = 1 + stat.shouru_zonge = amount + stat.save() + else: + GuanshiRiTongji.objects.filter(id=stat.id).update( + chongzhi_dashou_shu=F('chongzhi_dashou_shu') + 1, + shouru_zonge=F('shouru_zonge') + amount + ) + + elif action in (3, 4): + amount = Decimal(str(amount)) if amount else Decimal('0.00') + if amount <= 0: + return + if created: + stat.shouru_zonge = amount + stat.save() + else: + GuanshiRiTongji.objects.filter(id=stat.id).update( + shouru_zonge=F('shouru_zonge') + amount + ) + +'''def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')): + """ + 根据操作行为更新管事每日统计(邀请打手、充值会员) + + 参数: + yonghuid: 管事用户ID + action: 操作类型 + 1 = 邀请打手(次数自动+1,无需传其他参数) + 2 = 充值会员(需要传 amount 参数) + amount: 充值金额(Decimal),仅在 action=2 时有效 + + 说明: + - 每次调用只代表一次操作,不会批量处理。 + - 使用 select_for_update + F() 保证原子性和并发安全。 + """ + today = date.today() + + # 邀请打手:次数+1,金额不变 + if action == 1: + count_field = 'yaoqing_dashou_shu' + amount = Decimal('0.00') + + # 充值会员:次数+1,金额累加 + elif action == 2: + amount = Decimal(str(amount)) if amount else Decimal('0.00') + if amount <= 0: + return + count_field = 'chongzhi_dashou_shu' + + else: + return # 无效行为,直接返回 + + with transaction.atomic(): + stat, created = GuanshiRiTongji.objects.select_for_update().get_or_create( + yonghuid=yonghuid, + riqi=today, + defaults={ + 'yonghuid': yonghuid, + 'riqi': today, + 'yaoqing_dashou_shu': 0, + 'chongzhi_dashou_shu': 0, + 'shouru_zonge': Decimal('0.00'), + } + ) + + if created: + # 新记录直接赋初值 + if action == 1: + stat.yaoqing_dashou_shu = 1 + else: + # 已存在记录,使用 F 表达式原子累加次数 + update_fields = {count_field: F(count_field) + 1} + + if action == 2: + update_fields['shouru_zonge'] = F('shouru_zonge') + amount + + GuanshiRiTongji.objects.filter(id=stat.id).update(**update_fields)''' + +def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00')): + """ + 管事续费统计(单独表) + 用法: update_guanshi_xufei_daily('300001', Decimal('30')) + """ + today = date.today() + with transaction.atomic(): + stat, created = GuanshiXufeiRiTongji.objects.select_for_update().get_or_create( + yonghuid=yonghuid, + riqi=today, + defaults={ + 'yonghuid': yonghuid, + 'riqi': today, + 'xufei_zongshu': 1, + 'xufei_shouyi': xufei_jine, + } + ) + if not created: + GuanshiXufeiRiTongji.objects.filter(id=stat.id).update( + xufei_zongshu=F('xufei_zongshu') + 1, + xufei_shouyi=F('xufei_shouyi') + xufei_jine + ) + + +def update_zuzhang_daily_by_action(yonghuid, action, amount=Decimal('0.00')): + """ + 根据操作行为更新组长每日统计(邀请管事、分佣收入) + + 参数: + yonghuid: 组长用户ID + action: 操作类型 + 1 = 邀请管事(次数+1,无金额) + 2 = 分佣收入(累加金额) + amount: 分佣金额(Decimal),仅在 action=2 时有效 + """ + today = date.today() + + with transaction.atomic(): + stat, created = ZuzhangRiTongji.objects.select_for_update().get_or_create( + yonghuid=yonghuid, + riqi=today, + defaults={ + 'yonghuid': yonghuid, + 'riqi': today, + 'yaoqing_guanshi_shu': 0, + 'shouru_zonge': Decimal('0.00'), + 'fenyong_jine': Decimal('0.00'), + } + ) + + if created: + # 新记录直接设置初始值 + if action == 1: + stat.yaoqing_guanshi_shu = 1 + elif action == 2: + amount = Decimal(str(amount)) if amount else Decimal('0.00') + stat.shouru_zonge = amount + stat.fenyong_jine = amount + stat.save() + else: + # 已存在记录,使用 F 表达式原子累加 + if action == 1: + ZuzhangRiTongji.objects.filter(id=stat.id).update( + yaoqing_guanshi_shu=F('yaoqing_guanshi_shu') + 1 + ) + elif action == 2: + amount = Decimal(str(amount)) if amount else Decimal('0.00') + if amount > 0: + ZuzhangRiTongji.objects.filter(id=stat.id).update( + shouru_zonge=F('shouru_zonge') + amount, + fenyong_jine=F('fenyong_jine') + amount + ) + + +def update_tixian_daily_stat(leixing, amount): + """ + 更新每日提现统计(平台维度) + leixing: 1=打手, 2=管事, 3=组长 + amount: 本次提现金额(申请金额,含手续费) + """ + today = date.today() + with transaction.atomic(): + stat, created = TixianRiTongji.objects.select_for_update().get_or_create( + riqi=today, + leixing=leixing, + defaults={'total_amount': amount} + ) + if not created: + TixianRiTongji.objects.filter(id=stat.id).update( + total_amount=F('total_amount') + amount + ) + +def decrease_tixian_daily_stat(leixing, amount): + """ + 减少每日提现统计(平台维度),用于提现失败或取消时回滚 + :param leixing: 1=打手,2=管事,3=组长 + :param amount: 要减少的金额(实际到账金额) + """ + today = date.today() + with transaction.atomic(): + # 使用 select_for_update 防止并发回滚时数据错乱 + stat = TixianRiTongji.objects.select_for_update().get(riqi=today, leixing=leixing) + stat.total_amount -= amount + stat.save(update_fields=['total_amount']) + + diff --git a/houtai/view.py b/houtai/view.py new file mode 100644 index 0000000..7c2d00b --- /dev/null +++ b/houtai/view.py @@ -0,0 +1,12894 @@ +import threading +import traceback +from calendar import monthrange + +from django.contrib.auth.hashers import make_password +from django.db.models import Q, Count +from django.core.paginator import Paginator + + +from houtai.utils import update_dashou_daily_by_action +from dingdan.models import Lilubiao + +import uuid + +from rest_framework.parsers import JSONParser, MultiPartParser, FormParser + +from utils.oss_utils import upload_to_oss, validate_image, delete_from_oss +from shangpin.models import Shangpin, ShangpinLeixing, ShangpinZhuanqu,Huiyuan + + +import hashlib +from django.db.models import Q, Sum, F +from rest_framework import status + +from houtai.models import Role, Permission, RolePermission, UserRole, TixianRiTongji +from houtai.utils import verify_kefu_permission +import xmltodict + +from django.conf import settings + +from dingdan.models import Dashoutupian, Chufajilu, PreSettlement, OrderDashouHistory +# apps/houtai/views.py 继续添加 + +from django.views.decorators.csrf import csrf_exempt +from django.utils.decorators import method_decorator + +from houtai.models import Role + +from yonghu.models import UserMain, UserGuanshi, UserBoss, UserZuzhang + +from shangpin.models import DuociFenhong + +import time +import logging +import requests +from datetime import date, datetime, timedelta +from django.utils import timezone + +from decimal import Decimal + +from django.core.exceptions import ObjectDoesNotExist +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated, AllowAny + +from rest_framework.response import Response +from rest_framework.parsers import JSONParser + +from dingdan.utils import update_daily_dispatch_stat +from shangpin.models import ShangpinLeixing, Huiyuangoumai, Huiyuan +from yonghu.models import UserMain, UserKefu, UserDashou, Xiugaijilu, UserShangjia +from peizhi.models import AccountPermission, ClubConfig, Club, DailyDispatchStat, OrderTypeMapping, TixianQuotaDefault +from dingdan.models import Dingdan, Tuikuanjilu, CrossPlatformOrderData + +# views.py +import os +from peizhi.models import PopupPage, PopupConfig, PopupImage +from .serializers import PopupPageSerializer, PopupConfigSerializer, PopupImageSerializer + + +logger = logging.getLogger(__name__) + + +class GetClubConfigView(APIView): + """ + 获取对方俱乐部配置列表(用于跨平台选择) + 权限要求:客服身份 + 权限代码1且开启 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + """ + 处理 POST 请求,返回对方俱乐部配置数据(is_self=0) + """ + try: + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + # 5. 查询所有对方俱乐部(is_self=0),只取必要字段 + club_configs = ClubConfig.objects.filter( + is_self=0 # 只返回对方俱乐部 + ).only( + 'club_id', + 'club_nickname', + 'club_avatar', + 'storage_bucket_domain' # 如果前端需要存储桶域名,一并返回 + ) + + # 构造返回数据列表 + data_list = [] + for club in club_configs: + data_list.append({ + 'club_id': club.club_id, + 'club_nickname': club.club_nickname or club.club_id, # 若昵称为空,用ID代替 + 'club_avatar': club.club_avatar or '', # 可能为空 + 'storage_bucket_domain': club.storage_bucket_domain or '' # 可选 + }) + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': data_list + }) + + except Exception as e: + logger.error(f"获取俱乐部配置失败: {str(e)}") + return Response({ + 'code': 500, + 'msg': '服务器内部错误' + }) + + + + + +class GetCrossOrderListView(APIView): + """ + 获取跨平台订单列表(客服后台) + 权限要求:客服身份 + 权限代码1且开启 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + """ + 处理 POST 请求,返回分页订单数据及统计信息 + """ + try: + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部订单'}) + + # 5. 获取请求参数 + data = request.data + + + + + # 分页参数 + page = int(data.get('page', 1)) + page_size = int(data.get('pageSize', 20)) + + # 查询参数 + leixing = data.get('leixing') # 订单类型ID + zhuangtai = data.get('zhuangtai') # 状态字符串,如 "1,7" + club_id = data.get('club_id') # 对方俱乐部ID(精确) + direction = data.get('direction') # send/receive/all + dingdan_id = data.get('dingdan_id') # 订单ID模糊搜索 + club_id_search = data.get('club_id_search') # 对方俱乐部ID模糊搜索 + date = data.get('date') # 日期 YYYY-MM-DD + dashou_id = data.get('dashou_id') # 打手ID(接单打手ID) + clkf = data.get('clkf') # 处理客服 + + # 6. 构建基础查询(跨平台订单) + base_q = Q(is_cross=1) + + # 添加筛选条件(非空且非全部) + if leixing and leixing != '': + base_q &= Q(leixing_id=leixing) + + if zhuangtai and zhuangtai != 'all': + # 解析逗号分隔的状态码 + status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip().isdigit()] + if status_list: + base_q &= Q(zhuangtai__in=status_list) + + if club_id and club_id != '': + base_q &= Q(partner_club_id=club_id) + + # 方向映射 + if direction == 'send': + base_q &= Q(dispatch_type=1) + elif direction == 'receive': + base_q &= Q(dispatch_type=2) + # direction == 'all' 时不加方向条件 + + if dingdan_id: + base_q &= Q(dingdan_id__icontains=dingdan_id) + + if club_id_search: + base_q &= Q(partner_club_id__icontains=club_id_search) + + if date: + # 按日期精确过滤(假设 create_time 是带时区的 DateTimeField) + base_q &= Q(create_time__date=date) + + if dashou_id: + base_q &= Q(jiedan_dashou_id=dashou_id) + + if clkf: + base_q &= Q(clkf__icontains=clkf) + + # 7. 统计各状态数量(基于相同过滤条件) + # 获取所有符合条件的订单的状态分组统计 + status_counts = Dingdan.objects.filter(base_q).values('zhuangtai').annotate(count=Count('id')).order_by() + + # 转换为字典 {状态码: 数量} + status_map = {item['zhuangtai']: item['count'] for item in status_counts} + + # 定义状态列表(与前端一致) + status_list_map = { + 'all': 0, # 稍后填充总数量 + '1,7': 0, + '2': 0, + '8': 0, + '3': 0, + '4': 0, + '5': 0, + '6': 0, + } + # 填充总数 + status_list_map['all'] = sum(status_map.values()) + # 填充单个状态 + for code, cnt in status_map.items(): + if code == 1 or code == 7: + status_list_map['1,7'] += cnt + elif code in status_list_map: + status_list_map[str(code)] = cnt + # 其他状态暂不处理 + + # 构建 stats 统计 + total_orders = status_list_map['all'] + completed_orders = status_map.get(3, 0) + refund_orders = status_map.get(5, 0) + pending_orders = status_map.get(8, 0) + status_map.get(4, 0) + + stats = { + 'totalOrders': total_orders, + 'completedOrders': completed_orders, + 'refundOrders': refund_orders, + 'pendingOrders': pending_orders, + } + + # 8. 分页获取订单列表 + # 按创建时间倒序,保证分页稳定 + orders_qs = Dingdan.objects.filter(base_q).order_by('-create_time') + + # 使用 Paginator 分页 + paginator = Paginator(orders_qs, page_size) + current_page = paginator.get_page(page) + order_list = current_page.object_list + + # 只返回前端需要的字段,减少数据传输 + data_list = [] + for order in order_list: + data_list.append({ + 'dingdan_id': order.dingdan_id, + 'nicheng': order.nicheng or '', + 'jieshao': order.jieshao or '', + 'jiage': float(order.jine) if order.jine else 0, + 'zhuangtai': order.zhuangtai, + }) + + # 9. 返回结果 + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'list': data_list, + 'total': total_orders, + 'statusCounts': status_list_map, + 'stats': stats, + } + }) + + except Exception as e: + logger.error(f"获取跨平台订单列表失败: {str(e)}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + +class GetCrossOrderDetailView(APIView): + """ + 客服获取跨平台订单详情接口 + 请求:POST /houtai/hqkptddxq + 参数:{"username": "客服手机号", "dingdan_id": "订单ID"} + 认证:JWT,权限码1 + 返回:code=0 + 订单详情数据(包含俱乐部信息、打手图片、处罚记录等) + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + start_time = time.time() + + # 1. 获取参数 + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + if not username or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + # 2. 身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部订单'}) + + # 4. 查询订单 + dingdan_obj = Dingdan.objects.select_related( + 'pingtai_kuozhan', + 'shangjia_kuozhan' + ).get(dingdan_id=dingdan_id) + + # 5. 构建基础响应数据 + response_data = { + 'dingdan_id': dingdan_obj.dingdan_id or '', + 'zhuangtai': dingdan_obj.zhuangtai or 0, + 'fadanpingtai': dingdan_obj.fadan_pingtai or 0, + 'dispatch_type': dingdan_obj.dispatch_type or 0, # 1=我方派单, 2=对方派单 + 'is_cross': dingdan_obj.is_cross or 0, + 'partner_order_id': dingdan_obj.partner_order_id or '', + 'partner_club_id': dingdan_obj.partner_club_id or '', + 'jine': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00, + 'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00, + 'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '', + 'dashou_liuyan': dingdan_obj.dashou_liuyan or '', + 'zhiding_id': dingdan_obj.zhiding_id or '', + 'shangpin_id': dingdan_obj.shangpin_id or '', + 'leixing': dingdan_obj.leixing_id or 0, + 'tupian_url': dingdan_obj.tupian or '', + 'jieshao': dingdan_obj.jieshao or '', + 'beizhu': dingdan_obj.beizhu or '', + 'tkly': dingdan_obj.tkly or '', + 'nicheng': dingdan_obj.nicheng or '', + 'clkf': dingdan_obj.clkf or '', + 'chuangjianshijian': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.create_time else '', + 'genggaishijian': dingdan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.update_time else '', + } + + # 6. 扩展表数据 + if dingdan_obj.fadan_pingtai == 1: + if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan: + response_data['pingtai_kuozhan'] = { + 'laoban_id': dingdan_obj.pingtai_kuozhan.laoban_id or '', + 'laoban_pingjia': dingdan_obj.pingtai_kuozhan.laoban_pingjia or '', + } + else: + response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''} + elif dingdan_obj.fadan_pingtai == 2: + if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan: + shangjia = dingdan_obj.shangjia_kuozhan + response_data['shangjia_kuozhan'] = { + 'shangjia_id': shangjia.shangjia_id or '', + 'sjnicheng': shangjia.sjnicheng or '', + 'shangjia_pingjia': shangjia.shangjia_pingjia or '', + 'cfliyou': shangjia.cfliyou or '', + 'sqzhuangtai': shangjia.sqzhuangtai if shangjia.sqzhuangtai is not None else 0, + 'bhliyou': shangjia.bhliyou or '', + } + else: + response_data['shangjia_kuozhan'] = { + 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '', + 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': '' + } + + # ========== 核心逻辑:判断派单/接单关系 ========== + dispatch_type = dingdan_obj.dispatch_type # 1=我方派单, 2=对方派单 + is_cross = dingdan_obj.is_cross + partner_club_id = dingdan_obj.partner_club_id + partner_order_id = dingdan_obj.partner_order_id + + # 获取我方俱乐部配置(用于本地存储桶域名) + my_club_config = ClubConfig.objects.filter(is_self=1).first() + my_storage_domain = my_club_config.storage_bucket_domain if my_club_config else '' + + # 初始化字段 + response_data['club_info'] = None # 对方俱乐部信息 + response_data['partner_images'] = [] # 打手提交图片(对方平台打手或我方打手) + response_data['punishment_info'] = {} # 处罚信息 + response_data['is_punished'] = False # 是否已处罚(用于前端禁用按钮) + response_data['receive_side'] = '' # 接单方:'self' 我方接单,'partner' 对方接单 + response_data['dispatch_side'] = '' # 派单方:'self' 我方派单,'partner' 对方派单 + + # 设置派单方 + if dispatch_type == 1: + response_data['dispatch_side'] = 'self' + elif dispatch_type == 2: + response_data['dispatch_side'] = 'partner' + else: + # 兼容旧数据:根据 partner_club_id 或 partner_order_id 推断 + if partner_club_id or partner_order_id: + response_data['dispatch_side'] = 'partner' + else: + response_data['dispatch_side'] = 'self' + + # 情况1:对方派单(对方平台发单,我方打手接单) + if response_data['dispatch_side'] == 'partner': + # 接单方为我方 + response_data['receive_side'] = 'self' + + # 获取对方俱乐部信息(用于展示) + if partner_club_id: + partner_config = ClubConfig.objects.filter(club_id=partner_club_id, is_self=0).first() + if partner_config: + response_data['club_info'] = { + 'club_id': partner_config.club_id, + 'club_nickname': partner_config.club_nickname or partner_config.club_id, + 'club_avatar': partner_config.club_avatar or '', + } + else: + response_data['club_info'] = {'club_id': partner_club_id, 'club_nickname': '', 'club_avatar': ''} + else: + response_data['club_info'] = None + + # 查询我方打手提交的图片(本地) + dashou_images = Dashoutupian.objects.filter( + dingdan_id=dingdan_id, + dashou=dingdan_obj.jiedan_dashou_id + ).values_list('tupian', flat=True) + images = [] + for rel_path in dashou_images: + if rel_path: + if rel_path.startswith('http'): + images.append(rel_path) + else: + images.append(my_storage_domain + rel_path) + response_data['partner_images'] = images + + # 查询我方打手处罚记录 + punish_records = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + dashouid=dingdan_obj.jiedan_dashou_id + ).order_by('-create_time') + if punish_records.exists(): + latest = punish_records.first() + response_data['punishment_info'] = { + 'cfliyou': latest.cfliyou or '', + 'sqzhuangtai': latest.sqzhuangtai or 0, + 'bhliyou': latest.bhliyou or '', + 'ssliyou': latest.ssliyou or '', + 'jifen': latest.jifen or 0, + } + response_data['is_punished'] = True + else: + response_data['punishment_info'] = {} + response_data['is_punished'] = False + + # 情况2:我方派单(我方平台发单) + else: + response_data['dispatch_side'] = 'self' + # 根据是否有对方订单ID/俱乐部ID判断接单方 + if partner_club_id or partner_order_id: + # 有对方信息,说明订单派给了对方平台,对方接单 + response_data['receive_side'] = 'partner' + + # 获取对方俱乐部信息 + if partner_club_id: + partner_config = ClubConfig.objects.filter(club_id=partner_club_id, is_self=0).first() + if partner_config: + response_data['club_info'] = { + 'club_id': partner_config.club_id, + 'club_nickname': partner_config.club_nickname or partner_config.club_id, + 'club_avatar': partner_config.club_avatar or '', + } + else: + response_data['club_info'] = {'club_id': partner_club_id, 'club_nickname': '', 'club_avatar': ''} + + # 需要调用对方接口获取图片和处罚记录 + if partner_club_id and partner_order_id: + # 获取对方服务器域名 + if my_club_config: + self_club_id = my_club_config.club_id + else: + logger.error(f"无法获取本俱乐部ID,订单ID: {dingdan_id}") + return Response({'code': 500, 'msg': '系统配置错误,请联系管理员'}) + + try: + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain + except ObjectDoesNotExist: + logger.error(f"未找到合作关系: {self_club_id} <-> {partner_club_id}") + return Response({'code': 500, 'msg': '未找到对方平台配置'}) + + # 调用对方接口,最多重试3次 + url = partner_domain.rstrip('/') + '/houtai/kpthqtp' + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id, + } + timeout = 3 + retries = 3 + success = False + for attempt in range(retries): + try: + resp = requests.post(url, json=payload, timeout=timeout) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') == 0: + data = resp_json.get('data', {}) + response_data['partner_images'] = data.get('images', []) + response_data['punishment_info'] = data.get('punishment', {}) + response_data['is_punished'] = bool(response_data['punishment_info'].get('cfliyou')) + success = True + break + else: + logger.warning(f"对方接口返回错误 (尝试{attempt+1}): {resp_json.get('msg')}") + else: + logger.warning(f"对方接口HTTP错误 (尝试{attempt+1}): {resp.status_code}") + except requests.exceptions.Timeout: + logger.warning(f"请求对方接口超时 (尝试{attempt+1}): {url}") + except Exception as e: + logger.warning(f"请求对方接口异常 (尝试{attempt+1}): {str(e)}") + if not success: + # 三次失败,返回部分数据,但提示前端对方数据获取失败 + response_data['partner_images'] = [] + response_data['punishment_info'] = {} + response_data['is_punished'] = False + response_data['partner_data_error'] = '获取对方平台数据失败,请稍后重试' + else: + # 对方信息不完整,无法获取数据 + response_data['partner_images'] = [] + response_data['punishment_info'] = {} + response_data['is_punished'] = False + response_data['partner_data_error'] = '对方订单信息不完整' + else: + # 无对方信息,说明我方派单且我方接单 + response_data['receive_side'] = 'self' + # 我方打手提交图片(本地) + dashou_images = Dashoutupian.objects.filter( + dingdan_id=dingdan_id, + dashou=dingdan_obj.jiedan_dashou_id + ).values_list('tupian', flat=True) + images = [] + for rel_path in dashou_images: + if rel_path: + if rel_path.startswith('http'): + images.append(rel_path) + else: + images.append(my_storage_domain + rel_path) + response_data['partner_images'] = images + + # 处罚记录 + punish_records = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + dashouid=dingdan_obj.jiedan_dashou_id + ).order_by('-create_time') + if punish_records.exists(): + latest = punish_records.first() + response_data['punishment_info'] = { + 'cfliyou': latest.cfliyou or '', + 'sqzhuangtai': latest.sqzhuangtai or 0, + 'bhliyou': latest.bhliyou or '', + 'ssliyou': latest.ssliyou or '', + 'jifen': latest.jifen or 0, + } + response_data['is_punished'] = True + else: + response_data['punishment_info'] = {} + response_data['is_punished'] = False + + # 如果是我方派单且我方接单,对方俱乐部信息为空 + response_data['club_info'] = None + + # 7. 查询退款理由 + try: + tuikuan_record = Tuikuanjilu.objects.filter( + dingdan_id=dingdan_id + ).order_by('-create_time').first() + response_data['tuikuan_liyou'] = tuikuan_record.liyou if tuikuan_record else '' + except Exception as e: + logger.warning(f"查询退款记录失败: {str(e)}") + response_data['tuikuan_liyou'] = '' + + # ========== 新增:查询更换打手生成的新订单信息 ========== + try: + from peizhi.models import ShangjiaLianjie + new_lianjie = ShangjiaLianjie.objects.filter( + previous_dingdan_id=dingdan_id + ).order_by('-create_time').first() + if new_lianjie: + response_data['new_dingdan_id'] = new_lianjie.dingdan_id or '' + response_data['new_lianjie'] = new_lianjie.lianjie or '' + else: + response_data['new_dingdan_id'] = '' + response_data['new_lianjie'] = '' + except Exception as e: + logger.warning(f"查询新订单链接失败: {str(e)}") + response_data['new_dingdan_id'] = '' + response_data['new_lianjie'] = '' + # ===================================================== + + elapsed = time.time() - start_time + logger.info(f"获取跨平台订单详情接口耗时: {elapsed:.3f}s, 订单ID: {dingdan_id}") + return Response({'code': 0, 'data': response_data}) + + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {dingdan_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + except Exception as e: + logger.error(f"获取跨平台订单详情接口异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + +'''class CrossPlatformRefundView(APIView): + """ + 跨平台订单退款接口(同时支持本地订单和跨平台订单) + 请求:POST /houtai/kptk + 参数:{ + "username": "客服账号ID", + "dingdan_id": "订单ID", + "tuikuan_liyou": "退款理由(可选)" + } + 认证:JWT + 返回:code=0 成功,其他失败 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() + + if not username or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权处理订单'}) + + # 4. 查询订单(带扩展表) + try: + order = Dingdan.objects.select_related( + 'pingtai_kuozhan', + 'shangjia_kuozhan' + ).get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {dingdan_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + + # 5. 订单状态校验(只有 1,7,2,8,4 可退款) + allowed_statuses = [1, 7, 2, 8, 4] + if order.zhuangtai not in allowed_statuses: + logger.warning(f"订单状态不允许退款: {order.zhuangtai}") + return Response({'code': 400, 'msg': '订单状态不允许退款'}) + + # 6. 根据派单方向分流 + dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单 + + # ---------- 情况一:对方派单(我方打手接对方的单)---------- + if dispatch_type == 2: + # 对方派单,资金在对方平台,我方只处理打手状态和订单状态 + with transaction.atomic(): + # 更新订单状态为已退款 + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + + # 更新打手数据(接单打手) + dashou_id = order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 # 设为空闲 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"对方派单退款:打手 {dashou_id} 不存在,跳过更新") + + # 更新退款记录(记录操作) + try: + refund_record, created = Tuikuanjilu.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'dashouid': dashou_id or '', + 'qingqiuid': '', + 'chuliid': current_user.phone, + 'liyou': tuikuan_liyou or '客服退款(对方派单)', + 'sqzhuangtai': 1, + 'jine': order.jine or 0, + 'dashou_fencheng': order.dashou_fencheng or 0, + 'jieshao': order.jieshao or '', + 'beizhu': '对方派单退款', + 'nicheng': order.nicheng or '', + } + ) + if not created: + refund_record.sqzhuangtai = 1 + refund_record.chuliid = current_user.phone + if tuikuan_liyou: + refund_record.liyou = tuikuan_liyou + refund_record.save() + except Exception as e: + logger.error(f"更新退款记录失败: {str(e)}") + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=3 # 3 表示退款(退款) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + # ✅ 新方法(行为驱动,更安全) + from houtai.utils import update_shangjia_daily + shangjia_ext = order.shangjia_kuozhan + shangjia_id = shangjia_ext.shangjia_id + update_shangjia_daily( + yonghuid=shangjia_id, + amount=Decimal(str(order.jine)), + action=3 # 3 = 退款 + ) + + # 更新客服统计 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + logger.info(f"对方派单退款成功,订单 {dingdan_id}") + return Response({'code': 0, 'msg': '退款成功'}) + + # ---------- 情况二:我方派单(订单由我方发出)---------- + # 需要进一步判断是本地订单还是跨平台订单 + is_cross = order.is_cross + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + + # 子情况2.1:本地订单(非跨平台) + if not is_cross or not partner_club_id: + # 按原有逻辑处理:平台订单或商家订单 + if order.fadan_pingtai == 1: + # 平台订单退款(调用微信支付) + return self._refund_platform_order(order, current_user, tuikuan_liyou, kefu) + elif order.fadan_pingtai == 2: + # 商家订单退款(直接退余额) + return self._refund_merchant_order(order, current_user, tuikuan_liyou, kefu) + else: + return Response({'code': 400, 'msg': '未知的发单平台类型'}) + + # 子情况2.2:跨平台订单(我方派单给对方) + else: + # 必须先执行本地资金退款(因为钱在我方平台) + # 根据发单平台类型执行退款 + local_refund_success = False + try: + with transaction.atomic(): + if order.fadan_pingtai == 1: + # 平台订单:调用微信退款 + refund_result = self._call_wechat_refund(order) + if refund_result['code'] != 0: + raise Exception(refund_result['msg']) + # 微信退款成功,更新订单状态 + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + local_refund_success = True + elif order.fadan_pingtai == 2: + # 商家订单:退余额给商家 + self._refund_to_merchant(order, current_user.phone, tuikuan_liyou) + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + local_refund_success = True + else: + return Response({'code': 400, 'msg': '未知的发单平台类型'}) + + # 更新打手数据(接单打手) + dashou_id = order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"跨平台退款:打手 {dashou_id} 不存在,跳过更新") + + # 更新退款记录 + self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou) + + # 更新客服统计 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + except Exception as e: + logger.error(f"跨平台订单本地退款失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': f'本地退款失败: {str(e)}'}) + + # 本地退款成功后,通知对方平台 + if local_refund_success: + # 查询对方平台域名 + try: + # 获取我方俱乐部ID + my_club_config = ClubConfig.objects.filter(is_self=1).first() + if not my_club_config: + logger.error("未配置我方俱乐部信息") + # 本地已成功,仅记录错误,不影响返回 + else: + self_club_id = my_club_config.club_id + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain + + # 调用对方退款通知接口 + notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk' + payload = { + 'order_id': partner_order_id, # 对方订单ID + 'club_id': self_club_id, # 我方俱乐部ID + 'status': 5, # 通知对方我方已退款 + 'reason': tuikuan_liyou + } + # 设置超时3秒,不阻塞主流程 + try: + resp = requests.post(notify_url, json=payload, timeout=3) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') == 0: + # 更新跨平台订单扩展表 + cross_data, _ = CrossPlatformOrderData.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id, + 'partner_order_status': resp_json.get('data', {}).get('partner_status', 0), + 'partner_dashou_id': resp_json.get('data', {}).get('partner_dashou_id', ''), + 'partner_amount': order.jine or 0, + } + ) + if not _: + cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0) + cross_data.save() + else: + logger.warning(f"对方平台返回错误: {resp_json.get('msg')}") + else: + logger.warning(f"对方平台接口HTTP错误: {resp.status_code}") + except requests.exceptions.Timeout: + logger.error(f"通知对方平台超时: {notify_url}") + except Exception as e: + logger.error(f"通知对方平台异常: {str(e)}") + except Exception as e: + logger.error(f"查询对方平台配置失败: {str(e)}") + + + + return Response({'code': 0, 'msg': '退款成功'}) + + # ========== 辅助方法 ========== + def _refund_platform_order(self, order, current_user, tuikuan_liyou, kefu): + """平台订单退款(调用微信支付)""" + # 调用微信退款 + refund_result = self._call_wechat_refund(order) + if refund_result['code'] != 0: + return Response({'code': 400, 'msg': refund_result['msg']}) + + with transaction.atomic(): + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + + # 更新打手 + dashou_id = order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"平台退款:打手 {dashou_id} 不存在,跳过更新") + + # 更新退款记录 + self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou) + + is_cross = order.is_cross + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + + if not is_cross or not partner_order_id: + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=3 # 3 表示退款(退款) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + else: + pass + + + # 客服统计 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + logger.info(f"平台订单退款成功: {order.dingdan_id}") + return Response({'code': 0, 'msg': '退款成功'}) + + def _refund_merchant_order(self, order, current_user, tuikuan_liyou, kefu): + """商家订单退款(退余额给商家)""" + # 获取商家ID + try: + shangjia_ext = order.shangjia_kuozhan + shangjia_id = shangjia_ext.shangjia_id + except ObjectDoesNotExist: + return Response({'code': 400, 'msg': '订单商家信息不存在'}) + + with transaction.atomic(): + # 订单状态改已退款 + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + + # 退余额给商家 + try: + shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_profile = shangjia_user.shop_profile + shangjia_profile.tuikuan += 1 + shangjia_profile.yue += order.jine or 0 + shangjia_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.error(f"商家退款:商家 {shangjia_id} 不存在,无法退余额") + raise Exception("商家账户不存在") + + # 更新打手 + dashou_id = order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"商家退款:打手 {dashou_id} 不存在,跳过更新") + + # 更新退款记录 + self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou) + + is_cross = order.is_cross + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + + if not is_cross or not partner_order_id: + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=3 # 3 表示退款(退款) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + else: + pass + + # ✅ 新方法(行为驱动,更安全) + from houtai.utils import update_shangjia_daily + update_shangjia_daily( + yonghuid=shangjia_id, + amount=Decimal(str(order.jine)), + action=3 # 3 = 退款 + ) + + # 客服统计 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + logger.info(f"商家订单退款成功: {order.dingdan_id}") + return Response({'code': 0, 'msg': '退款成功'}) + + def _call_wechat_refund(self, order): + """调用微信支付退款接口,返回 {'code':0, 'msg':'success'} 或错误信息""" + # 获取微信支付配置 + appid = getattr(settings, 'WEIXIN_APPID', '') + mch_id = getattr(settings, 'WEIXIN_MCHID', '') + key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') + cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '') + key_path = getattr(settings, 'WEIXIN_KEY_PATH', '') + + if not all([appid, mch_id, key, cert_path, key_path]): + missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v] + logger.error(f"微信支付配置缺失: {missing}") + return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'} + + # 生成退款单号 + out_refund_no = self._generate_refund_no(order.dingdan_id) + total_fee = int(float(order.jine or 0) * 100) + refund_fee = total_fee + refund_desc = '客服退款' + + params = { + 'appid': appid, + 'mch_id': mch_id, + 'nonce_str': self._generate_nonce_str(), + 'out_refund_no': out_refund_no, + 'total_fee': total_fee, + 'refund_fee': refund_fee, + 'refund_desc': refund_desc, + } + # 优先使用微信交易号 + if order.wechat_transaction_id: + params['transaction_id'] = order.wechat_transaction_id + else: + params['out_trade_no'] = order.dingdan_id + + # 签名 + sorted_keys = sorted(params.keys()) + stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys]) + stringSignTemp = f"{stringA}&key={key}" + sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() + params['sign'] = sign + + xml_data = self._dict_to_xml(params) + url = 'https://api.mch.weixin.qq.com/secapi/pay/refund' + + try: + response = requests.post(url, data=xml_data, cert=(cert_path, key_path), timeout=10) + result = xmltodict.parse(response.content)['xml'] + logger.info(f"微信退款响应: {result}") + except Exception as e: + logger.error(f"微信退款请求异常: {str(e)}", exc_info=True) + return {'code': 500, 'msg': '微信退款请求异常'} + + if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': + return {'code': 0, 'msg': 'success'} + else: + err_msg = result.get('err_code_des', result.get('return_msg', '未知错误')) + logger.warning(f"微信退款失败: {err_msg}") + return {'code': 400, 'msg': err_msg} + + def _refund_to_merchant(self, order, clkf_phone, tuikuan_liyou): + """给商家退余额(事务内调用,不再独立事务)""" + try: + shangjia_ext = order.shangjia_kuozhan + shangjia_id = shangjia_ext.shangjia_id + except ObjectDoesNotExist: + raise Exception("订单商家信息不存在") + + try: + shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_profile = shangjia_user.shop_profile + shangjia_profile.tuikuan += 1 + shangjia_profile.yue += order.jine or 0 + shangjia_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.error(f"商家退款:商家 {shangjia_id} 不存在") + raise Exception("商家账户不存在") + + def _update_refund_record(self, order, dashou_id, chuli_phone, tuikuan_liyou): + """更新退款记录表""" + try: + #更新支出记录 + if order.fadan_pingtai == 1: + from dingdan.utils import update_daily_payout + update_daily_payout(order.jine) + + + # ========== 新增:商品和店铺每日统计 ========== + try: + from shangdian.utils import update_dianpu_daily_stat + from shangpin.utils import update_shangpin_daily_stat + # 商品每日统计(只要有商品ID就执行) + if order.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(order, 'pingtai_kuozhan'): + pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=order.shangpin_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + dianpu_id = None + if hasattr(order, 'pingtai_kuozhan'): + dianpu_id = order.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + + + refund_record, created = Tuikuanjilu.objects.get_or_create( + dingdan_id=order.dingdan_id, + defaults={ + 'dashouid': dashou_id or '', + 'qingqiuid': '', + 'chuliid': chuli_phone, + 'liyou': tuikuan_liyou or '客服退款', + 'sqzhuangtai': 1, + 'jine': order.jine or 0, + 'dashou_fencheng': order.dashou_fencheng or 0, + 'jieshao': order.jieshao or '', + 'beizhu': '客服退款', + 'nicheng': order.nicheng or '', + } + ) + if not created: + refund_record.sqzhuangtai = 1 + refund_record.chuliid = chuli_phone + if tuikuan_liyou: + refund_record.liyou = tuikuan_liyou + refund_record.save() + except Exception as e: + logger.error(f"更新退款记录异常: {str(e)}") + + def _generate_nonce_str(self): + return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + def _generate_refund_no(self, dingdan_id): + timestamp = str(int(time.time())) + rand = ''.join(random.choices('0123456789', k=4)) + return f"{dingdan_id}REF{timestamp}{rand}" + + def _dict_to_xml(self, data): + xml = [''] + for k, v in data.items(): + xml.append(f'<{k}>{v}') + xml.append('') + return ''.join(xml)''' + +import hmac +import hashlib +import json +import logging +import time +import random +import string +import traceback +import requests +import xmltodict +from decimal import Decimal + +from django.conf import settings +from django.db import transaction +from django.core.cache import cache +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 django.core.exceptions import ObjectDoesNotExist + +from yonghu.models import UserMain +from dingdan.models import Dingdan, Tuikuanjilu, CrossPlatformOrderData +from shangpin.models import Shangpin +from peizhi.models import Szjilu, ClubConfig, Club +from dingdan.utils import ( + update_daily_payout +) +from .utils import verify_kefu_permission + +logger = logging.getLogger(__name__) + + +class CrossPlatformRefundView(APIView): + """ + 跨平台订单退款接口(同时支持本地订单和跨平台订单) + 请求:POST /houtai/kptk + 参数:{ + "username": "客服账号ID", + "dingdan_id": "订单ID", + "tuikuan_liyou": "退款理由(可选)" + } + 认证:JWT + 返回:code=0 成功,其他失败 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() + + if not username or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 身份验证 + current_user = request.user + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权处理订单'}) + + # 4. 查询订单(带扩展表) + try: + order = Dingdan.objects.select_related( + 'pingtai_kuozhan', + 'shangjia_kuozhan' + ).get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {dingdan_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + + # 5. 订单状态校验(只有 1,7,2,8,4 可退款) + allowed_statuses = [1, 7, 2, 8, 4] + if order.zhuangtai not in allowed_statuses: + logger.warning(f"订单状态不允许退款: {order.zhuangtai}") + return Response({'code': 400, 'msg': '订单状态不允许退款'}) + + # 6. 根据派单方向分流 + dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单 + + # ---------- 情况一:对方派单(我方打手接对方的单)---------- + if dispatch_type == 2: + # 对方派单,资金在对方平台,我方只处理打手状态和订单状态 + with transaction.atomic(): + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + + dashou_id = order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"对方派单退款:打手 {dashou_id} 不存在,跳过更新") + + try: + refund_record, created = Tuikuanjilu.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'dashouid': dashou_id or '', + 'qingqiuid': '', + 'chuliid': current_user.phone, + 'liyou': tuikuan_liyou or '客服退款(对方派单)', + 'sqzhuangtai': 1, + 'jine': order.jine or 0, + 'dashou_fencheng': order.dashou_fencheng or 0, + 'jieshao': order.jieshao or '', + 'beizhu': '对方派单退款', + 'nicheng': order.nicheng or '', + } + ) + if not created: + refund_record.sqzhuangtai = 1 + refund_record.chuliid = current_user.phone + if tuikuan_liyou: + refund_record.liyou = tuikuan_liyou + refund_record.save() + except Exception as e: + logger.error(f"更新退款记录失败: {str(e)}") + + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + logger.info(f"对方派单退款成功,订单 {dingdan_id}") + return Response({'code': 0, 'msg': '退款成功'}) + + # ---------- 情况二:我方派单(订单由我方发出)---------- + is_cross = order.is_cross + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + + # 子情况2.1:本地订单(非跨平台) + if not is_cross or not partner_club_id: + if order.fadan_pingtai == 1: + # 平台订单退款 + return self._refund_platform_order(order, current_user, tuikuan_liyou, kefu) + elif order.fadan_pingtai == 2: + # 商家订单退款 + return self._refund_merchant_order(order, current_user, tuikuan_liyou, kefu) + else: + return Response({'code': 400, 'msg': '未知的发单平台类型'}) + + # 子情况2.2:跨平台订单(我方派单给对方) + else: + local_refund_success = False + try: + with transaction.atomic(): + if order.fadan_pingtai == 1: + refund_result = self._call_wechat_refund(order) + if refund_result['code'] != 0: + raise Exception(refund_result['msg']) + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + local_refund_success = True + elif order.fadan_pingtai == 2: + self._refund_to_merchant(order, current_user.phone, tuikuan_liyou) + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + local_refund_success = True + else: + return Response({'code': 400, 'msg': '未知的发单平台类型'}) + + dashou_id = order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"跨平台退款:打手 {dashou_id} 不存在,跳过更新") + + self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou) + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + except Exception as e: + logger.error(f"跨平台订单本地退款失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': f'本地退款失败: {str(e)}'}) + + # 本地退款成功后,通知对方平台 + if local_refund_success: + try: + my_club_config = ClubConfig.objects.filter(is_self=1).first() + if not my_club_config: + logger.error("未配置我方俱乐部信息") + else: + self_club_id = my_club_config.club_id + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain + + notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk' + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id, + 'status': 5, + 'reason': tuikuan_liyou + } + try: + resp = requests.post(notify_url, json=payload, timeout=3) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') == 0: + cross_data, _ = CrossPlatformOrderData.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id, + 'partner_order_status': resp_json.get('data', {}).get('partner_status', 0), + 'partner_dashou_id': resp_json.get('data', {}).get('partner_dashou_id', ''), + 'partner_amount': order.jine or 0, + } + ) + if not _: + cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0) + cross_data.save() + else: + logger.warning(f"对方平台返回错误: {resp_json.get('msg')}") + else: + logger.warning(f"对方平台接口HTTP错误: {resp.status_code}") + except requests.exceptions.Timeout: + logger.error(f"通知对方平台超时: {notify_url}") + except Exception as e: + logger.error(f"通知对方平台异常: {str(e)}") + except Exception as e: + logger.error(f"查询对方平台配置失败: {str(e)}") + + return Response({'code': 0, 'msg': '退款成功'}) + + # ========== 辅助方法 ========== + def _refund_platform_order(self, order, current_user, tuikuan_liyou, kefu): + """平台订单退款(调用微信支付)""" + refund_result = self._call_wechat_refund(order) + if refund_result['code'] != 0: + return Response({'code': 400, 'msg': refund_result['msg']}) + + with transaction.atomic(): + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + + dashou_id = order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"平台退款:打手 {dashou_id} 不存在,跳过更新") + + self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou) + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + logger.info(f"平台订单退款成功: {order.dingdan_id}") + return Response({'code': 0, 'msg': '退款成功'}) + + def _refund_merchant_order(self, order, current_user, tuikuan_liyou, kefu): + """商家订单退款(退余额给商家)""" + try: + shangjia_ext = order.shangjia_kuozhan + shangjia_id = shangjia_ext.shangjia_id + except ObjectDoesNotExist: + return Response({'code': 400, 'msg': '订单商家信息不存在'}) + + with transaction.atomic(): + order.zhuangtai = 5 + order.clkf = current_user.phone + if tuikuan_liyou: + order.tkly = tuikuan_liyou + order.save() + + try: + shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_profile = shangjia_user.shop_profile + shangjia_profile.tuikuan += 1 + shangjia_profile.yue += order.jine or 0 + shangjia_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.error(f"商家退款:商家 {shangjia_id} 不存在,无法退余额") + raise Exception("商家账户不存在") + + dashou_id = order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"商家退款:打手 {dashou_id} 不存在,跳过更新") + + self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou) + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + logger.info(f"商家订单退款成功: {order.dingdan_id}") + return Response({'code': 0, 'msg': '退款成功'}) + + def _call_wechat_refund(self, order): + """ + 调用虚拟支付退款接口 — 完全模仿 KefuPlatformRefundView 的 call_virtual_refund 方法 + 使用 /xpay/refund_order 接口,字段名、请求体结构、签名方式与成功接口保持一致 + """ + uri = "/xpay/refund_order" + base_url = "https://api.weixin.qq.com" + appkey = settings.VP_APPKEY_SANDBOX if settings.VP_ENV == 1 else settings.VP_APPKEY + + # 金额:元 -> 分 + refund_fee = int(float(order.jine or 0) * 100) + + # 获取老板 openid(和成功接口完全相同的路径) + laoban_openid = None + laoban_id = order.pingtai_kuozhan.laoban_id if hasattr(order, 'pingtai_kuozhan') else None + if laoban_id: + laoban_user = UserMain.objects.filter(yonghuid=laoban_id).first() + if laoban_user: + laoban_openid = getattr(laoban_user, 'openid', None) + + if not laoban_openid: + return {'code': 400, 'msg': '未找到用户openid,无法退款'} + + # 退款单号:R + 时间戳 + 6位随机数(和成功接口一模一样) + refund_order_id = f"R{int(time.time())}{random.randint(100000, 999999)}" + + # 请求体:字段名、顺序与成功接口完全一致 + body_data = { + "openid": laoban_openid, + "env": settings.VP_ENV, + "order_id": order.dingdan_id, # 商户订单号(和成功接口一样) + "refund_order_id": refund_order_id, # 本次退款单号(和成功接口一样) + "left_fee": refund_fee, # 订单剩余可退金额(和成功接口一样) + "refund_fee": refund_fee, # 本次退款金额(和成功接口一样) + "biz_meta": "客服退款", # 业务备注(和成功接口一样) + "refund_reason": 5, # 退款原因:5 其他(和成功接口一样) + "req_from": 1 # 退款来源:1 人工客服(和成功接口一样) + } + # 如果有微信支付交易单号,优先传入(和成功接口一样) + if getattr(order, 'wechat_transaction_id', None): + body_data["wx_order_id"] = order.wechat_transaction_id + + body = json.dumps(body_data, separators=(",", ":")) + + # 签名:pay_sig(和成功接口一模一样) + msg = uri + "&" + body + pay_sig = hmac.new(appkey.encode("utf-8"), msg.encode("utf-8"), hashlib.sha256).hexdigest() + + # 获取 access_token + access_token = self._get_access_token() + + # 构建 URL:和成功接口一模一样 + url = f"{base_url}{uri}?access_token={access_token}&pay_sig={pay_sig}" + + headers = {"Content-Type": "application/json"} + try: + response = requests.post(url, data=body, headers=headers, timeout=10) + result = response.json() + + if result.get("errcode") == 0: + return {'code': 0, 'msg': '退款成功', 'refund_id': refund_order_id} + else: + errmsg = result.get("errmsg", "未知错误") + logger.error(f"[退款] 退款失败: errcode={result.get('errcode')}, errmsg={errmsg}") + return {'code': 400, 'msg': f"退款失败: {errmsg}"} + + except requests.exceptions.RequestException as e: + logger.error(f"[退款] 网络请求异常: {e}") + return {'code': 500, 'msg': '微信退款请求网络异常'} + + def _get_access_token(self): + """获取 access_token,带缓存(与成功接口相同)""" + cache_key = "virtual_payment_access_token" + access_token = cache.get(cache_key) + if not access_token: + logger.info("[退款] 缓存无 access_token,重新获取") + resp = requests.get( + "https://api.weixin.qq.com/cgi-bin/token", + params={ + "grant_type": "client_credential", + "appid": settings.WEIXIN_APPID, + "secret": settings.WEIXIN_SECRET, + }, + timeout=10, + ) + data = resp.json() + if "access_token" in data: + access_token = data["access_token"] + cache.set(cache_key, access_token, data.get("expires_in", 7200) - 300) + logger.info(f"[退款] access_token 已更新") + else: + raise Exception(f"获取 access_token 失败: {data}") + return access_token + + def _refund_to_merchant(self, order, clkf_phone, tuikuan_liyou): + """给商家退余额(事务内调用,不再独立事务)""" + try: + shangjia_ext = order.shangjia_kuozhan + shangjia_id = shangjia_ext.shangjia_id + except ObjectDoesNotExist: + raise Exception("订单商家信息不存在") + + try: + shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_profile = shangjia_user.shop_profile + shangjia_profile.tuikuan += 1 + shangjia_profile.yue += order.jine or 0 + shangjia_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.error(f"商家退款:商家 {shangjia_id} 不存在") + raise Exception("商家账户不存在") + + def _update_refund_record(self, order, dashou_id, chuli_phone, tuikuan_liyou): + """更新退款记录表""" + try: + if order.fadan_pingtai == 1: + from dingdan.utils import update_daily_payout + update_daily_payout(order.jine) + + refund_record, created = Tuikuanjilu.objects.get_or_create( + dingdan_id=order.dingdan_id, + defaults={ + 'dashouid': dashou_id or '', + 'qingqiuid': '', + 'chuliid': chuli_phone, + 'liyou': tuikuan_liyou or '客服退款', + 'sqzhuangtai': 1, + 'jine': order.jine or 0, + 'dashou_fencheng': order.dashou_fencheng or 0, + 'jieshao': order.jieshao or '', + 'beizhu': '客服退款', + 'nicheng': order.nicheng or '', + } + ) + if not created: + refund_record.sqzhuangtai = 1 + refund_record.chuliid = chuli_phone + if tuikuan_liyou: + refund_record.liyou = tuikuan_liyou + refund_record.save() + except Exception as e: + logger.error(f"更新退款记录异常: {str(e)}") + + def _generate_nonce_str(self): + return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + def _generate_refund_no(self, dingdan_id): + timestamp = str(int(time.time())) + rand = ''.join(random.choices('0123456789', k=4)) + return f"{dingdan_id}REF{timestamp}{rand}" + + def _dict_to_xml(self, data): + xml = [''] + for k, v in data.items(): + xml.append(f'<{k}>{v}') + xml.append('') + return ''.join(xml) + + + + + + + + + + + + + + + +class PartnerGetOrderDataView(APIView): + """ + 对方平台调用,获取我方订单的打手图片和处罚记录 + 请求:POST /houtai/kpthqtp + 参数:{ + "order_id": "对方平台的订单ID", + "club_id": "对方俱乐部ID" + } + 无需认证 + 返回:code=0 + images列表 + punishment信息 + """ + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + try: + # 1. 获取参数 + partner_order_id = request.data.get('order_id', '').strip() + partner_club_id = request.data.get('club_id', '').strip() + if not partner_order_id or not partner_club_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 查询我方存储桶域名(自己俱乐部的配置) + my_club_config = ClubConfig.objects.filter(is_self=1).first() + if not my_club_config: + logger.error("未配置我方俱乐部存储桶域名") + return Response({'code': 500, 'msg': '系统配置错误'}) + storage_domain = my_club_config.storage_bucket_domain or '' + + # 3. 查询我方订单(通过对方订单ID和对方俱乐部ID) + try: + order = Dingdan.objects.get( + dingdan_id=partner_order_id, + partner_club_id=partner_club_id + ) + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: partner_order_id={partner_order_id}, partner_club_id={partner_club_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + + dingdan_id = order.dingdan_id + dashou_id = order.jiedan_dashou_id + + # 4. 查询打手图片(绝对URL) + images = [] + if dashou_id: + dashou_images = Dashoutupian.objects.filter( + dingdan_id=dingdan_id, + dashou=dashou_id + ).values_list('tupian', flat=True) + for rel_path in dashou_images: + if rel_path: + if rel_path.startswith('http'): + images.append(rel_path) + else: + images.append(storage_domain + rel_path) + + # 5. 查询处罚记录(最新一条) + punishment = { + 'cfliyou': '', + 'sqzhuangtai': 0, + 'bhliyou': '', + 'ssliyou': '', + 'jifen': 0 + } + if dashou_id: + punish_record = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + dashouid=dashou_id + ).order_by('-create_time').first() + if punish_record: + punishment = { + 'cfliyou': punish_record.cfliyou or '', + 'sqzhuangtai': punish_record.sqzhuangtai or 0, + 'bhliyou': punish_record.bhliyou or '', + 'ssliyou': punish_record.ssliyou or '', + 'jifen': punish_record.jifen or 0, + } + + # 6. 返回数据 + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'images': images, + 'punishment': punishment + } + }) + + except Exception as e: + logger.error(f"对方获取订单数据接口异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + +class PartnerRefundNotifyView(APIView): + """ + 接收对方平台推送的订单状态变更通知(完成/退款),同步更新我方数据。 + 请求:POST /houtai/dfddtk + 参数: + { + "order_id": "对方平台订单ID(实际为我方订单ID)", + "club_id": "对方俱乐部ID", + "status": 3 或 5, + "reason": "理由(可选)" + } + 返回:code=0 + data包含我方订单当前状态和打手ID + + 核心职责: + 1. 更新跨平台订单扩展表 + 2. 对于对方派单(我方打手接对方的单): + a. 状态3(完成)→ 更新订单为已完成,创建预结算(如无) + b. 状态5(退款)→ 更新订单为已退款,将预结算待结算改为已拒绝 + c. 在此过程中,若订单原状态为“进行中(2)”,则将打手状态重置为空闲(1) + """ + + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 参数校验(事务外执行) + partner_order_id = request.data.get('order_id', '').strip() + partner_club_id = request.data.get('club_id', '').strip() + partner_status = request.data.get('status') + reason = request.data.get('reason', '').strip() + + if not partner_order_id or not partner_club_id: + return Response({'code': 400, 'msg': '参数不完整'}) + if partner_status not in (3, 5): + return Response({'code': 400, 'msg': '不支持的状态码'}) + + # 2. 核心业务在事务内执行 + try: + with transaction.atomic(): + # 2.1 锁定订单行(必须事务内) + try: + order = Dingdan.objects.select_for_update().get( + dingdan_id=partner_order_id, + partner_club_id=partner_club_id + ) + except Dingdan.DoesNotExist: + logger.warning(f"通知订单不存在: order_id={partner_order_id}, club_id={partner_club_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + + my_dingdan_id = order.dingdan_id + dashou_id = order.jiedan_dashou_id or '' + + # 记录原订单状态,用于后续打手状态重置 + original_order_status = order.zhuangtai + + # 2.2 更新跨平台扩展表(记录对方状态) + cross_data, created = CrossPlatformOrderData.objects.get_or_create( + dingdan_id=my_dingdan_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id, + 'partner_order_status': partner_status, + 'partner_dashou_id': '', + 'partner_amount': order.jine or 0, + } + ) + if not created: + cross_data.partner_order_status = partner_status + cross_data.save(update_fields=['partner_order_status', 'update_time']) + + # 2.3 仅处理对方派单(dispatch_type == 2) + if order.dispatch_type == 2: + # ========== 打手状态恢复逻辑 ========== + # 如果原订单状态为“进行中(2)”,且存在接单打手,则将打手状态改为空闲(1) + if original_order_status == 2 and dashou_id: + try: + # 通过打手ID查用户主表,再反向获取打手扩展表 + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.zhuangtai = 1 # 状态置为空闲 + dashou_profile.save(update_fields=['zhuangtai']) + logger.info(f"打手 {dashou_id} 状态已重置为1(空闲)") + + + + except UserMain.DoesNotExist: + logger.warning(f"打手用户不存在: {dashou_id}") + except UserDashou.DoesNotExist: + logger.warning(f"打手扩展信息不存在: {dashou_id}") + + # 🔥 新增:调用打手每日统计(成交) + if partner_status ==3: + + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=2 # 3 表示退款(退款) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + if partner_status ==5: + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=order.dashou_fencheng, + action=3 # 3 表示退款(退款) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + + # 不影响主流程 + + # ========== 情况A:对方完成(status=3) ========== + if partner_status == 3: + order.zhuangtai = 3 + + order.clkf = partner_club_id # 处理人(对方俱乐部ID) + order.save(update_fields=['zhuangtai', 'update_time','clkf']) + + if dashou_id: + yjs, yjs_created = PreSettlement.objects.get_or_create( + dingdan_id=my_dingdan_id, + dashou_id=dashou_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id, + 'amount': order.dashou_fencheng or 0, + 'order_amount': order.jine or 0, + 'status': 0, # 待结算 + 'operator': '', + } + ) + if yjs_created: + logger.info(f"预结算创建成功: {my_dingdan_id}, 打手 {dashou_id}, 金额 {order.dashou_fencheng}") + else: + logger.info(f"预结算已存在,跳过: {my_dingdan_id}, 打手 {dashou_id}") + else: + logger.warning(f"订单 {my_dingdan_id} 无接单打手,跳过预结算创建") + + # ========== 情况B:对方退款(status=5) ========== + elif partner_status == 5: + order.zhuangtai = 5 + order.tkly = reason if reason else '' # 退款理由 + order.clkf = partner_club_id # 处理人(对方俱乐部ID) + order.save(update_fields=['zhuangtai', 'update_time','clkf','tkly']) + + if dashou_id: + try: + yjs = PreSettlement.objects.get( + dingdan_id=my_dingdan_id, + dashou_id=dashou_id + ) + if yjs.status == 0: # 待结算 → 已拒绝 + yjs.status = 2 + yjs.process_time = timezone.now() + yjs.save(update_fields=['status', 'process_time', 'update_time']) + logger.info(f"预结算状态已改为已拒绝: {my_dingdan_id}") + except PreSettlement.DoesNotExist: + logger.info(f"订单 {my_dingdan_id} 无预结算记录,无需修改") + + logger.info(f"对方通知处理完成: dingdan_id={my_dingdan_id}, partner_status={partner_status}") + + # 返回结果 + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'partner_status': order.zhuangtai, + 'partner_dashou_id': dashou_id + } + }) + + except Exception as e: + logger.error(f"对方通知接口异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + +from houtai.utils import update_dashou_daily_by_action + +class CrossPlatformForceCompleteView(APIView): + """ + 跨平台订单强制结单接口 + 请求:POST /houtai/kpqzjd + 参数:{"username": "客服账号ID", "dingdan_id": "订单ID"} + 认证:JWT + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + if not username or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查处理订单'}) + + # 4. 查询订单并加锁 + try: + with transaction.atomic(): + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + # 5. 校验订单状态必须为8(结算中) + if order.zhuangtai not in (2,8): + return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'}) + + # 获取关键字段 + dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单 + is_cross = order.is_cross # 0=本地,1=跨平台 + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + dashou_id = order.jiedan_dashou_id + dashou_fencheng = order.dashou_fencheng or Decimal('0') + jine = order.jine or Decimal('0') + fadan_pingtai = order.fadan_pingtai # 1=平台,2=商家 + + # ========== 修复点1:完善跨平台判断 ========== + # 跨平台且被对方打手接单的条件:is_cross==1 且 (partner_club_id 或 partner_order_id 有值) + is_cross_claimed_by_partner = (is_cross == 1 and (partner_club_id or partner_order_id)) + # 如果是跨平台订单但两个字段都为空,则按本地订单处理(即自家打手接单) + + # ========== 修复点2:根据方向处理 ========== + # ---------- 对方派单(我方打手接对方单)---------- + if dispatch_type == 2: + # 创建预结算记录(如果已存在则更新) + pre_settlement, created = PreSettlement.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id, + 'dashou_id': dashou_id, + 'amount': dashou_fencheng, + 'order_amount': jine, + 'status': 0, # 待结算 + 'operator': current_user.phone, + } + ) + if not created: + pre_settlement.partner_order_id = partner_order_id + pre_settlement.partner_club_id = partner_club_id + pre_settlement.dashou_id = dashou_id + pre_settlement.amount = dashou_fencheng + pre_settlement.order_amount = jine + pre_settlement.status = 0 + pre_settlement.process_time = None + pre_settlement.operator = current_user.phone + pre_settlement.save() + + # 更新订单状态 + order.zhuangtai = 3 + order.clkf = current_user.phone + order.save() + + # 更新每日统计表(对方派单方向,使用订单金额) + if partner_club_id: + # 修复点3:使用公共统计函数 + update_daily_dispatch_stat( + direction=2, + partner_club_id=partner_club_id, + success_amount=jine, + success_count=1 + ) + else: + # 如果没有俱乐部ID,使用默认000001 + update_daily_dispatch_stat( + direction=2, + partner_club_id='000001', + success_amount=jine, + success_count=1 + ) + + # 通知对方平台订单已完成(修复点4:移到事务外) + if partner_club_id and partner_order_id: + transaction.on_commit( + lambda: self._notify_partner_order_status( + partner_club_id=partner_club_id, + partner_order_id=partner_order_id, + new_status=3, + current_user=current_user + ) + ) + + # ---------- 我方派单 ---------- + elif dispatch_type == 1: + # 判断是否为跨平台订单且被对方打手接单(即 partner_club_id 或 partner_order_id 有值) + if is_cross_claimed_by_partner: + # 跨平台订单:我方派单给外平台打手 + # 修复点5:处理只有 partner_order_id 没有 partner_club_id 的情况 + real_partner_club_id = partner_club_id or '000001' + + # 更新订单状态 + order.zhuangtai = 3 + order.clkf = current_user.phone + order.save() + + # 更新每日统计表(我方派单方向,使用打手分成金额) + update_daily_dispatch_stat( + direction=1, + partner_club_id=real_partner_club_id, + success_amount=dashou_fencheng, + success_count=1 + ) + + # ========== 新增:商品和店铺每日统计 ========== + + if order.fadan_pingtai == 1: + try: + from shangdian.utils import update_dianpu_daily_stat + from shangpin.utils import update_shangpin_daily_stat + # 商品每日统计(只要有商品ID就执行) + if order.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(order, 'pingtai_kuozhan'): + pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=order.shangpin_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + dianpu_id = None + if hasattr(order, 'pingtai_kuozhan'): + dianpu_id = order.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + # 通知对方平台订单已完成(异步) + if partner_order_id: + transaction.on_commit( + lambda: self._notify_partner_order_status( + partner_club_id=real_partner_club_id, + partner_order_id=partner_order_id, + new_status=3, + current_user=current_user + ) + ) + else: + # 本地订单(非跨平台 或 跨平台但实际是自家打手接单),按原有逻辑给自家打手结算 + if not dashou_id: + return Response({'code': 400, 'msg': '订单无接单打手'}) + if dashou_fencheng < 0: + return Response({'code': 400, 'msg': '打手分成金额无效'}) + # ========== 新增:商品和店铺每日统计 ========== + if order.fadan_pingtai == 1: + try: + from shangdian.utils import update_dianpu_daily_stat + from shangpin.utils import update_shangpin_daily_stat + # 商品每日统计(只要有商品ID就执行) + if order.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(order, 'pingtai_kuozhan'): + pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=order.shangpin_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + dianpu_id = None + if hasattr(order, 'pingtai_kuozhan'): + dianpu_id = order.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + # 更新打手余额和统计 + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.chengjiaozongliang += 1 + dashou_profile.yue += dashou_fencheng + dashou_profile.zonge += dashou_fencheng + dashou_profile.jinrishouyi += dashou_fencheng + dashou_profile.jinyueshouyi += dashou_fencheng + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"本地订单结算:打手 {dashou_id} 不存在,跳过") + + # 如果是商家订单,更新商家成交单量 + if fadan_pingtai == 2: + try: + shangjia_ext = order.shangjia_kuozhan + if shangjia_ext and shangjia_ext.shangjia_id: + shangjia_user = UserMain.objects.get(yonghuid=shangjia_ext.shangjia_id) + shangjia_profile = shangjia_user.shop_profile + shangjia_profile.chengjiao += 1 + shangjia_profile.save() + except Exception as e: + logger.warning(f"商家订单更新失败: {str(e)}") + + # 更新订单状态 + order.zhuangtai = 3 + order.clkf = current_user.phone + order.save() + + # 本地订单不涉及跨平台统计,无需更新 DailyDispatchStat + + else: + return Response({'code': 400, 'msg': '未知的派单方向'}) + + if dispatch_type == 1 and is_cross_claimed_by_partner: + pass + # 判断是否为跨平台订单且被对方打手接单(即 partner_club_id 或 partner_order_id 有值) + elif dispatch_type == 2: + pass + else: + + + # 🔥 新增:调用打手每日统计(成交) + try: + update_dashou_daily_by_action( + yonghuid=order.jiedan_dashou_id, + amount=dashou_fencheng, + action=2 # 2 表示成交(结算) + ) + logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功") + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + # 不影响主流程 + + if order.fadan_pingtai == 2: + shangjia_ext = order.shangjia_kuozhan + # ✅ 新方法(行为驱动,更安全) + from houtai.utils import update_shangjia_daily + update_shangjia_daily( + yonghuid=shangjia_ext.shangjia_id, + amount=Decimal(str(order.jine)), + action=2 # 2 = 结算 + ) + else: + pass + + + # 7. 更新客服统计 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + # 8. 记录操作日志 + logger.info(f"强制结单成功,订单 {dingdan_id},方向 {dispatch_type},客服 {username}") + + return Response({'code': 0, 'msg': '强制结单成功'}) + + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {dingdan_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + except Exception as e: + logger.error(f"强制结单接口异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + # ========== 辅助方法 ========== + def _notify_partner_order_status(self, partner_club_id, partner_order_id, new_status, current_user): + """ + 通知对方平台订单状态变更(已在事务外调用) + """ + try: + # 获取我方俱乐部ID + my_club_config = ClubConfig.objects.filter(is_self=1).first() + if not my_club_config: + logger.error("未配置我方俱乐部信息,无法通知对方") + return + self_club_id = my_club_config.club_id + + # 查询对方服务器域名 + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain + if not partner_domain: + logger.error(f"对方俱乐部 {partner_club_id} 未配置域名") + return + + url = partner_domain.rstrip('/') + '/houtai/dfddtk' + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id, + 'status': new_status + } + resp = requests.post(url, json=payload, timeout=3) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') == 0: + # 更新跨平台订单扩展表中的对方状态 + cross_data, _ = CrossPlatformOrderData.objects.get_or_create( + dingdan_id=partner_order_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id, + 'partner_order_status': new_status, + 'partner_dashou_id': '', + 'partner_amount': Decimal('0') + } + ) + if not _: + cross_data.partner_order_status = new_status + cross_data.save(update_fields=['partner_order_status']) + logger.info(f"通知对方订单状态成功: {partner_order_id} -> {new_status}") + else: + logger.warning(f"对方接口返回错误: {resp_json.get('msg')}") + else: + logger.warning(f"对方接口HTTP错误: {resp.status_code}") + except requests.exceptions.Timeout: + logger.error(f"通知对方平台超时: {partner_club_id}") + except Exception as e: + logger.error(f"通知对方平台异常: {str(e)}") + + + + +class CrossPlatformTransferHallView(APIView): + """ + 跨平台订单转移大厅接口 + 请求:POST /houtai/kpzydt + 参数:{"username": "客服账号ID", "dingdan_id": "订单ID"} + 认证:JWT + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + if not username or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 客服身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权处理俱乐部订单'}) + + # 4. 查询订单并加锁 + try: + with transaction.atomic(): + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + # 5. 状态校验 + if order.zhuangtai not in [2, 8, 4]: + return Response({'code': 400, 'msg': f'订单状态不允许转移大厅,当前状态: {order.zhuangtai}'}) + + # 获取关键字段 + is_cross = order.is_cross + dispatch_type = order.dispatch_type + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + current_dashou_id = order.jiedan_dashou_id + zhiding_id = order.zhiding_id + + # ========== 判断是否需要通知对方平台 ========== + # 条件:跨平台 且 我方派单(dispatch_type=1) 且 (partner_club_id 或 partner_order_id 有值) + need_notify = (is_cross == 1 and (partner_club_id or partner_order_id)) + + # ========== 判断是否需要修改本地打手状态 ========== + # 当接单打手是我方打手时,才需要修改状态(改为空闲1) + # 我方打手的情况: + # 1. 非跨平台订单 (is_cross != 1) + # 2. 跨平台且对方派单 (dispatch_type == 2) —— 此时打手是我方 + # 3. 跨平台且我方派单但没有对方信息 (即 need_notify == False) —— 此时打手也是我方 + need_update_dashou_status = (current_dashou_id and + (is_cross != 1 or dispatch_type == 2 or not need_notify)) + + # ========== 记录打手历史(无论打手是谁,只要有ID就记录) ========== + if current_dashou_id: + last_history = OrderDashouHistory.objects.filter( + dingdan_id=dingdan_id + ).order_by('-times').first() + next_times = (last_history.times + 1) if last_history else 1 + OrderDashouHistory.objects.create( + dingdan_id=dingdan_id, + dashou_id=current_dashou_id, + times=next_times, + operator=username + ) + + # ========== 修改本地打手状态(仅当是我方打手时) ========== + if need_update_dashou_status: + try: + dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id) + dashou_profile.zhuangtai = 1 + dashou_profile.save() + logger.info(f"打手 {current_dashou_id} 状态改为空闲") + except ObjectDoesNotExist: + logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新") + + # ========== 更新订单状态(清空打手,改为1或7) ========== + if zhiding_id: + order.zhuangtai = 7 # 指定中 + else: + order.zhuangtai = 1 # 下单中 + order.jiedan_dashou_id = None + order.clkf = username + order.save() + + # ========== 如果需要通知对方平台,异步发送通知 ========== + if need_notify: + # 确定对方俱乐部ID(如果没有则用默认000001) + real_partner_club_id = partner_club_id if partner_club_id else '000001' + real_partner_order_id = partner_order_id if partner_order_id else dingdan_id + + # 获取我方俱乐部ID + my_club_config = ClubConfig.objects.filter(is_self=1).first() + if my_club_config: + self_club_id = my_club_config.club_id + # 查询对方域名 + try: + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=real_partner_club_id) + partner_domain = club_rel.partner_domain + if partner_domain: + url = partner_domain.rstrip('/') + '/houtai/kpttzzydt' + payload = { + 'order_id': real_partner_order_id, + 'club_id': self_club_id + } + + def notify(): + try: + resp = requests.post(url, json=payload, timeout=3) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') == 0: + # 更新跨平台订单扩展表中的对方状态 + cross_data, created = CrossPlatformOrderData.objects.get_or_create( + dingdan_id=dingdan_id, + defaults={ + 'partner_order_id': real_partner_order_id, + 'partner_club_id': real_partner_club_id, + 'partner_order_status': resp_json.get('data', {}).get('partner_status', 0), + 'partner_dashou_id': '', + 'partner_amount': order.jine or 0, + } + ) + if not created: + cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0) + cross_data.save(update_fields=['partner_order_status']) + logger.info(f"转移大厅通知对方成功: {dingdan_id}") + + # ========== 新增代码开始 ========== + # 对方成功转移大厅,清空我方订单中的对方相关信息 + # ========== 新增代码开始 ========== + # 只有我方派单时才清空对方平台信息,对方派单则保留 + if dispatch_type == 1: + Dingdan.objects.filter(dingdan_id=dingdan_id).update( + partner_club_id=None, + partner_order_id=None, + jiedan_dashou_id=None, + zhuangtai=1 + ) + logger.info(f"我方派单,已清空订单 {dingdan_id} 的对方信息") + else: + # 对方派单,只清空打手并恢复状态,保留对方信息 + Dingdan.objects.filter(dingdan_id=dingdan_id).update( + jiedan_dashou_id=None, + zhuangtai=1 + ) + logger.info(f"对方派单,只清空打手,保留对方信息,订单 {dingdan_id}") + # ========== 新增代码结束 ========== + # ========== 新增代码结束 ========== + + else: + logger.warning(f"对方平台返回错误: {resp_json.get('msg')}") + else: + logger.warning(f"对方接口HTTP错误: {resp.status_code}") + except Exception as e: + logger.error(f"转移大厅通知对方异常: {str(e)}") + + threading.Thread(target=notify).start() + except Club.DoesNotExist: + logger.error(f"未找到对方俱乐部配置: partner_club_id={real_partner_club_id}") + except Exception as e: + logger.error(f"查询对方平台配置失败: {str(e)}") + + # ========== 更新客服统计 ========== + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + return Response({'code': 0, 'msg': '转移大厅成功'}) + + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}) + except Exception as e: + logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + +class PartnerTransferHallNotifyView(APIView): + """ + 对方平台转移大厅通知接口 + 对方调用此接口通知我方订单已转移大厅,我方同步更新订单状态 + 请求:POST /houtai/kpttzzydt + 参数:{ + "order_id": "对方平台的订单ID", + "club_id": "对方俱乐部ID" + } + 无需认证 + 返回:{ + "code": 0, + "msg": "success", + "data": { + "partner_status": 1 # 我方订单当前状态 + } + } + """ + parser_classes = [JSONParser] + permission_classes = [AllowAny] + + def post(self, request): + try: + # 1. 获取参数 + partner_order_id = request.data.get('order_id', '').strip() + partner_club_id = request.data.get('club_id', '').strip() + if not partner_order_id or not partner_club_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 查询我方订单 + try: + order = Dingdan.objects.get( + dingdan_id=partner_order_id, + partner_club_id=partner_club_id + ) + except Dingdan.DoesNotExist: + logger.warning(f"对方转移大厅通知:订单不存在 partner_order_id={partner_order_id}, partner_club_id={partner_club_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + + # 3. 加锁处理 + with transaction.atomic(): + order = Dingdan.objects.select_for_update().get(id=order.id) + + current_status = order.zhuangtai + current_dashou_id = order.jiedan_dashou_id + + # 4. 根据订单状态处理 + if current_status == 3: + # 已完成状态,需要检查预结算记录 + try: + pre_settlement = PreSettlement.objects.select_for_update().get( + dingdan_id=order.dingdan_id, + dashou_id=current_dashou_id + ) + # 如果预结算状态是0(待结算),改为2(已拒绝) + if pre_settlement.status == 0: + pre_settlement.status = 2 + pre_settlement.process_time = timezone.now() + pre_settlement.operator = partner_club_id # 记录操作方 + pre_settlement.save() + logger.info(f"预结算记录已拒绝: {order.dingdan_id}") + else: + logger.info(f"预结算记录已是其他状态 {pre_settlement.status},不重复修改") + except PreSettlement.DoesNotExist: + logger.warning(f"订单 {order.dingdan_id} 已完成但无预结算记录,无法转移大厅") + return Response({'code': 400, 'msg': '订单已完成且无预结算记录,无法转移大厅'}) + + # 5. 记录打手历史(如果有接单打手) + if current_dashou_id: + last_history = OrderDashouHistory.objects.filter( + dingdan_id=order.dingdan_id + ).order_by('-times').first() + next_times = (last_history.times + 1) if last_history else 1 + OrderDashouHistory.objects.create( + dingdan_id=order.dingdan_id, + dashou_id=current_dashou_id, + times=next_times, + operator=partner_club_id # 记录操作方 + ) + # 将打手状态改为空闲 + try: + dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id) + dashou_profile.zhuangtai = 1 + dashou_profile.save() + + # 打手退款每日统计 + if order.dispatch_type == 2: + try: + update_dashou_daily_by_action( + yonghuid=current_dashou_id, + amount=dashou_profile.dashou_fencheng, + action=3 # 退款 + ) + except Exception as e: + logger.error(f"记录错误: {str(e)}") + except ObjectDoesNotExist: + logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新") + + # 7. 根据派单方向清理对方信息 + if order.dispatch_type == 1: + # 我方派单:清空对方俱乐部ID和订单ID + order.partner_club_id = None + order.partner_order_id = None + # 对方派单时保留对方信息,不清空 + + # 8. 更新订单状态 + if order.zhiding_id: + order.zhuangtai = 7 # 指定中 + else: + order.zhuangtai = 1 # 下单中 + + + + + + + order.jiedan_dashou_id = None + order.clkf = partner_club_id # 记录操作方 + order.save() + + logger.info(f"对方转移大厅通知成功: 订单 {order.dingdan_id},新状态 {order.zhuangtai}") + + # 7. 返回我方订单状态 + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'partner_status': order.zhuangtai + } + }) + + except Exception as e: + logger.error(f"对方转移大厅通知接口异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + + +logger = logging.getLogger(__name__) + + +# ========== 1. 获取俱乐部设置数据接口 ========== +class GetSettingsView(APIView): + """ + 获取跨平台设置页面数据 + 请求:POST /houtai/hqkptsz + 参数:{"username": "客服账号ID"} + 权限:权限码2且开启 + 返回:包含自己俱乐部信息(含客服字段)和合作俱乐部列表 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 客服身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + # 4. 查询数据 + try: + # 4.1 自己俱乐部配置(is_self=1) + self_config = ClubConfig.objects.filter(is_self=1).first() + if not self_config: + logger.error("未找到自己俱乐部配置") + return Response({'code': 500, 'msg': '系统配置错误'}) + + self_club = { + 'club_id': self_config.club_id, + 'club_nickname': self_config.club_nickname, + 'club_avatar': self_config.club_avatar, + 'backend_domain': self_config.backend_domain, + 'chat_api_key': self_config.chat_api_key, + 'chat_api_id': self_config.chat_api_id, + 'customer_service_api_url': self_config.customer_service_api_url, + 'customer_service_corp_id': self_config.customer_service_corp_id, + 'storage_bucket_domain': self_config.storage_bucket_domain, + } + + # 4.2 合作俱乐部列表(Club 表中本俱乐部ID = self_config.club_id) + partner_records = Club.objects.filter(club_id=self_config.club_id) + partner_clubs = [] + for record in partner_records: + partner_config = ClubConfig.objects.filter( + club_id=record.partner_club_id, + is_self=0 + ).first() + partner_clubs.append({ + 'club_id': record.partner_club_id, + 'club_nickname': partner_config.club_nickname if partner_config else record.partner_club_id, + 'club_avatar': partner_config.club_avatar if partner_config else '', + 'is_interop_enabled': record.is_interop_enabled, + 'is_receive_enabled': record.is_receive_enabled, + }) + + return Response({'code': 0, 'data': {'self_club': self_club, 'partner_clubs': partner_clubs}}) + + except Exception as e: + logger.error(f"获取设置失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + +# ========== 2. 获取订单类型映射接口 ========== +class GetMappingsView(APIView): + """ + 获取指定合作俱乐部的订单类型映射 + 请求:POST /houtai/hqqtjlbsj + 参数:{"username": "客服账号ID", "partner_club_id": "对方俱乐部ID"} + 权限:权限码2 + 返回:对方订单类型列表、我方订单类型列表、当前映射关系 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + partner_club_id = request.data.get('partner_club_id', '').strip() + if not username or not partner_club_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 客服身份验证 + current_user = request.user + + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + try: + # 4. 获取我方所有商品类型(订单类型) + our_types = ShangpinLeixing.objects.all().values('id', 'jieshao', 'tupian_url') + our_type_list = [ + {'type_id': t['id'], 'type_name': t['jieshao'], 'icon': t['tupian_url']} + for t in our_types + ] + + # 5. 获取对方订单类型(从映射表去重) + mappings_query = OrderTypeMapping.objects.filter(partner_club_id=partner_club_id) + partner_types_dict = {} + for m in mappings_query: + partner_types_dict[m.partner_type_id] = { + 'type_id': m.partner_type_id, + 'type_name': m.partner_type_name, + 'icon': '' + } + partner_type_list = list(partner_types_dict.values()) + + # 6. 获取当前映射关系 + mapping_list = [ + {'partner_type_id': m.partner_type_id, 'our_type_id': m.our_type_id} + for m in mappings_query + ] + + return Response({ + 'code': 0, + 'data': { + 'partner_types': partner_type_list, + 'our_types': our_type_list, + 'mappings': mapping_list + } + }) + + except Exception as e: + logger.error(f"获取映射失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + +# ========== 3. 保存设置接口(自己俱乐部 + 合作俱乐部开关)========== +class UpdateSettingsView(APIView): + """ + 保存俱乐部设置(自己俱乐部信息 + 合作俱乐部开关) + 请求:POST /houtai/xgjlbsj + 参数:{ + "username": "客服账号ID", + "self_club": { + "club_nickname": "...", + "backend_domain": "...", + "chat_api_key": "...", + "chat_api_id": "...", + "customer_service_api_url": "...", + "customer_service_corp_id": "...", + "storage_bucket_domain": "..." + }, + "partner_updates": [ + {"club_id": "654321", "is_interop_enabled": true, "is_receive_enabled": false} + ] + } + 权限:权限码2 + 返回:{"code": 0, "msg": "success"} + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 客服身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权修改俱乐部配置'}) + + # 4. 获取自己俱乐部当前配置 + try: + self_config = ClubConfig.objects.get(is_self=1) + except ObjectDoesNotExist: + logger.error("未找到自己俱乐部配置") + return Response({'code': 500, 'msg': '系统配置错误'}) + + # 5. 更新自己俱乐部配置(包含客服字段) + self_updates = request.data.get('self_club', {}) + if self_updates: + allowed_fields = [ + 'club_nickname', 'backend_domain', 'chat_api_key', 'chat_api_id', + 'customer_service_api_url', 'customer_service_corp_id', + 'storage_bucket_domain' + ] + fields_to_update = [] + for field in allowed_fields: + if field in self_updates: + setattr(self_config, field, self_updates[field]) + fields_to_update.append(field) + if fields_to_update: + self_config.save(update_fields=fields_to_update) + + # 6. 更新合作俱乐部开关 + partner_updates = request.data.get('partner_updates', []) + for update in partner_updates: + club_id = update.get('club_id') + if not club_id: + continue + try: + partner_record = Club.objects.get(club_id=self_config.club_id, partner_club_id=club_id) + if 'is_interop_enabled' in update: + partner_record.is_interop_enabled = update['is_interop_enabled'] + if 'is_receive_enabled' in update: + partner_record.is_receive_enabled = update['is_receive_enabled'] + partner_record.save(update_fields=['is_interop_enabled', 'is_receive_enabled']) + except ObjectDoesNotExist: + logger.warning(f"合作俱乐部记录不存在: {club_id}") + + # 7. 异步通知所有合作俱乐部(如果自己数据有修改) + if self_updates: + self._notify_all_partners(self_config, self_updates) + + return Response({'code': 0, 'msg': 'success'}) + + def _notify_all_partners(self, self_config, updates): + """异步通知所有合作俱乐部更新我方配置""" + partner_records = Club.objects.filter(club_id=self_config.club_id) + if not partner_records: + return + + data_to_send = { + 'club_id': self_config.club_id, + 'updates': updates + } + + def notify(partner_domain): + try: + url = partner_domain.rstrip('/') + '/houtai/xgzjsjcs' + resp = requests.post(url, json=data_to_send, timeout=3) + if resp.status_code != 200: + logger.warning(f"通知 {partner_domain} 失败,状态码 {resp.status_code}") + except Exception as e: + logger.error(f"通知 {partner_domain} 异常: {str(e)}") + + for record in partner_records: + domain = record.partner_domain + if domain: + threading.Thread(target=notify, args=(domain,)).start() + + + + + +class UpdateMappingsView(APIView): + """ + 保存指定合作俱乐部的订单类型映射 + 请求:POST /houtai/xgdfddys + 参数:{ + "username": "客服手机号", + "partner_club_id": "对方俱乐部ID", + "mappings": [ + {"partner_type_id": 1, "our_type_id": 10}, + {"partner_type_id": 2, "our_type_id": 11} + ] + } + 权限:权限码2 + 返回:{"code": 0, "msg": "success"} + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + partner_club_id = request.data.get('partner_club_id', '').strip() + mappings = request.data.get('mappings', []) + + if not username or not partner_club_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 客服身份验证 + current_user = request.user + + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权修改俱乐部配置'}) + + try: + with transaction.atomic(): + # 4. 获取当前该俱乐部所有现有映射(按 partner_type_id 索引) + existing_mappings = { + m.partner_type_id: m + for m in OrderTypeMapping.objects.filter(partner_club_id=partner_club_id) + } + + # 5. 处理前端传来的映射(更新或新增) + processed_partner_ids = set() + for mapping in mappings: + partner_type_id = mapping.get('partner_type_id') + our_type_id = mapping.get('our_type_id') + if partner_type_id is None or our_type_id is None: + continue + processed_partner_ids.add(partner_type_id) + + # 获取我方类型名称 + our_type_name = '' + try: + our_type = ShangpinLeixing.objects.get(id=our_type_id) + our_type_name = our_type.jieshao + except ObjectDoesNotExist: + logger.warning(f"我方订单类型不存在: {our_type_id}") + + # 查找是否存在该对方类型的映射 + if partner_type_id in existing_mappings: + # 更新现有记录 + mapping_obj = existing_mappings[partner_type_id] + mapping_obj.our_type_id = our_type_id + mapping_obj.our_type_name = our_type_name + mapping_obj.save(update_fields=['our_type_id', 'our_type_name']) + # 从字典中移除,以便后续知道哪些被删除了 + del existing_mappings[partner_type_id] + else: + # 新增记录 + # 对方类型名称:优先从现有映射中查找(可能被其他俱乐部共享?不,每个俱乐部独立) + # 由于该对方类型没有映射过,我们暂时无法获取名称,可留空或使用默认值。 + # 但为保证数据完整性,我们尝试从已有记录中查找(如果该俱乐部之前删除过又重建,则可能丢失名称) + # 更稳妥的方式是前端传递 partner_type_name,但当前未要求,因此此处留空并记录警告。 + partner_type_name = '' + logger.warning(f"新增映射时对方类型名称缺失: partner_club_id={partner_club_id}, partner_type_id={partner_type_id}") + + OrderTypeMapping.objects.create( + partner_club_id=partner_club_id, + partner_type_id=partner_type_id, + partner_type_name=partner_type_name, + our_type_id=our_type_id, + our_type_name=our_type_name + ) + + # 6. 删除前端未传入的映射(即用户取消的映射) + for partner_type_id, mapping_obj in existing_mappings.items(): + mapping_obj.delete() + logger.info(f"删除映射: partner_club_id={partner_club_id}, partner_type_id={partner_type_id}") + + return Response({'code': 0, 'msg': 'success'}) + + except Exception as e: + logger.error(f"保存映射失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) +# ========== 5. 接收对方平台修改数据接口 ========== +class PartnerUpdateSelfConfigView(APIView): + """ + 接收对方平台修改自己俱乐部配置的通知 + 请求:POST /houtai/xgzjsjcs + 参数:{ + "club_id": "对方俱乐部ID", + "updates": { + "club_nickname": "新昵称", + "backend_domain": "新域名", + "chat_api_key": "新key", + "chat_api_id": "新id", + "customer_service_api_url": "新URL", + "customer_service_corp_id": "新企业ID", + "storage_bucket_domain": "新存储桶" + } + } + 无需认证,对方平台可信调用 + 返回:{"code": 0, "msg": "success"} + """ + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + club_id = request.data.get('club_id', '').strip() + updates = request.data.get('updates', {}) + + if not club_id or not updates: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 查询对方俱乐部的配置记录(is_self=0) + try: + config = ClubConfig.objects.get(club_id=club_id, is_self=0) + except ClubConfig.DoesNotExist: + logger.warning(f"对方俱乐部配置不存在: {club_id}") + return Response({'code': 404, 'msg': '俱乐部不存在'}) + + # 3. 允许修改的字段白名单(包含客服字段) + allowed_fields = [ + 'club_nickname', + 'backend_domain', + 'chat_api_key', + 'chat_api_id', + 'customer_service_api_url', + 'customer_service_corp_id', + 'storage_bucket_domain' + ] + + # 4. 更新字段 + with transaction.atomic(): + fields_updated = [] + for field in allowed_fields: + if field in updates: + new_value = updates[field] + setattr(config, field, new_value) + fields_updated.append(field) + if fields_updated: + config.save(update_fields=fields_updated) + logger.info(f"对方俱乐部 {club_id} 配置已更新: {fields_updated}") + else: + logger.info(f"对方俱乐部 {club_id} 无有效字段更新") + + return Response({'code': 0, 'msg': 'success'}) + + + + +# apps/houtai/views.py + + + + + +# ========== 1. 获取合作俱乐部列表接口(完整) ========== +class GetPartnerClubsView(APIView): + """ + 获取我方合作俱乐部列表(用于统计页面) + 请求:POST /houtai/kpthqpt + 参数:{"username": "客服手机号"} + 权限:权限码2且开启 + 返回:{"code":0, "data":[{"club_id":"xxx","club_nickname":"xxx","club_avatar":"xxx"}]} + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 客服身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004bb' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + # 4. 获取我方俱乐部ID + try: + self_config = ClubConfig.objects.get(is_self=1) + except ObjectDoesNotExist: + logger.error("未找到自己俱乐部配置") + return Response({'code': 500, 'msg': '系统配置错误'}) + + # 5. 查询所有合作俱乐部 + try: + records = Club.objects.filter(club_id=self_config.club_id) + result = [] + for record in records: + partner_config = ClubConfig.objects.filter( + club_id=record.partner_club_id, + is_self=0 + ).first() + result.append({ + 'club_id': record.partner_club_id, + 'club_nickname': partner_config.club_nickname if partner_config else record.partner_club_id, + 'club_avatar': partner_config.club_avatar if partner_config else '', + }) + return Response({'code': 0, 'data': result}) + except Exception as e: + logger.error(f"查询合作俱乐部失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + +# ========== 2. 获取统计数据接口(完整) ========== + + + + + +class GetStatsView(APIView): + """ + 获取派单统计数据(按日或按月) + 请求:POST /houtai/hqpfsj + 参数:{ + "username": "客服手机号", + "club_id": "对方俱乐部ID", + "direction": 1, # 1=我方派单,2=对方派单 + "granularity": "day", # "day" 或 "month" + # 趋势图参数(可选,不传则使用默认值): + "trend_year_month": "YYYY-MM", # 按日时表示月份,缺省当前月 + "trend_year": "YYYY", # 按月时表示年份,缺省当前年 + # KPI参数(可选,不传则使用默认值): + "date": "YYYY-MM-DD", # 按日时表示日期,缺省当天 + "year_month": "YYYY-MM" # 按月时表示月份,缺省当前月 + } + 权限:权限码2 + 返回:{ + "code": 0, + "data": { + "summary": { ... }, + "chart": { ... } + } + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取基础参数 + username = request.data.get('username', '').strip() + club_id = request.data.get('club_id', '').strip() + direction = request.data.get('direction') + granularity = request.data.get('granularity') + + # 基础参数校验 + if not username: + return Response({'code': 400, 'msg': 'username 不能为空'}) + if not club_id: + return Response({'code': 400, 'msg': 'club_id 不能为空'}) + if direction is None: + return Response({'code': 400, 'msg': 'direction 不能为空'}) + if granularity not in ('day', 'month'): + return Response({'code': 400, 'msg': 'granularity 必须是 day 或 month'}) + try: + direction = int(direction) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': 'direction 必须为整数'}) + + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004bb' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + # 4. 获取当前时间用于默认值 + now = datetime.now() + + # 5. 处理趋势图参数(默认当前月/年) + if granularity == 'day': + trend_year_month = request.data.get('trend_year_month') + if not trend_year_month: + trend_year_month = now.strftime('%Y-%m') + try: + year, month = map(int, trend_year_month.split('-')) + start_date = datetime(year, month, 1).date() + _, days_in_month = monthrange(year, month) + end_date = datetime(year, month, days_in_month).date() + except (ValueError, TypeError): + # 如果格式错误,回退到当前月 + year, month = now.year, now.month + start_date = datetime(year, month, 1).date() + _, days_in_month = monthrange(year, month) + end_date = datetime(year, month, days_in_month).date() + else: + trend_year = request.data.get('trend_year') + if not trend_year: + trend_year = str(now.year) + try: + year = int(trend_year) + except (ValueError, TypeError): + year = now.year + start_date = datetime(year, 1, 1).date() + end_date = datetime(year, 12, 31).date() + + # 6. 处理KPI参数(默认当天/当月) + if granularity == 'day': + date_str = request.data.get('date') + if not date_str: + date_str = now.strftime('%Y-%m-%d') + try: + target_date = datetime.strptime(date_str, '%Y-%m-%d').date() + except (ValueError, TypeError): + target_date = now.date() + else: + year_month_str = request.data.get('year_month') + if not year_month_str: + year_month_str = now.strftime('%Y-%m') + try: + year, month = map(int, year_month_str.split('-')) + start_date_kpi = datetime(year, month, 1).date() + _, days_in_month = monthrange(year, month) + end_date_kpi = datetime(year, month, days_in_month).date() + except (ValueError, TypeError): + year, month = now.year, now.month + start_date_kpi = datetime(year, month, 1).date() + _, days_in_month = monthrange(year, month) + end_date_kpi = datetime(year, month, days_in_month).date() + + # 7. 查询趋势图数据 + response_data = { + 'summary': {'dispatch_count': 0, 'dispatch_amount': 0.0, 'success_count': 0, 'success_amount': 0.0}, + 'chart': {'labels': [], 'dispatch_counts': [], 'dispatch_amounts': [], 'success_counts': [], 'success_amounts': []} + } + + try: + if granularity == 'day': + qs = DailyDispatchStat.objects.filter( + partner_club_id=club_id, + direction=direction, + date__gte=start_date, + date__lte=end_date + ).order_by('date') + data_dict = {} + for item in qs: + data_dict[item.date] = { + 'dispatch_count': item.dispatch_count, + 'dispatch_amount': float(item.dispatch_amount), + 'success_count': item.success_count, + 'success_amount': float(item.success_amount), + } + labels = [] + dispatch_counts = [] + dispatch_amounts = [] + success_counts = [] + success_amounts = [] + current = start_date + while current <= end_date: + labels.append(current.strftime('%d')) + day_data = data_dict.get(current, {}) + dispatch_counts.append(day_data.get('dispatch_count', 0)) + dispatch_amounts.append(day_data.get('dispatch_amount', 0.0)) + success_counts.append(day_data.get('success_count', 0)) + success_amounts.append(day_data.get('success_amount', 0.0)) + current += timedelta(days=1) + response_data['chart'] = { + 'labels': labels, + 'dispatch_counts': dispatch_counts, + 'dispatch_amounts': dispatch_amounts, + 'success_counts': success_counts, + 'success_amounts': success_amounts, + } + else: # month + qs = DailyDispatchStat.objects.filter( + partner_club_id=club_id, + direction=direction, + date__gte=start_date, + date__lte=end_date + ).order_by('date') + month_totals = {m: {'dispatch_count': 0, 'dispatch_amount': 0.0, 'success_count': 0, 'success_amount': 0.0} + for m in range(1, 13)} + for item in qs: + m = item.date.month + month_totals[m]['dispatch_count'] += item.dispatch_count + month_totals[m]['dispatch_amount'] += float(item.dispatch_amount) + month_totals[m]['success_count'] += item.success_count + month_totals[m]['success_amount'] += float(item.success_amount) + labels = [] + dispatch_counts = [] + dispatch_amounts = [] + success_counts = [] + success_amounts = [] + for m in range(1, 13): + labels.append(f"{m}月") + dispatch_counts.append(month_totals[m]['dispatch_count']) + dispatch_amounts.append(month_totals[m]['dispatch_amount']) + success_counts.append(month_totals[m]['success_count']) + success_amounts.append(month_totals[m]['success_amount']) + response_data['chart'] = { + 'labels': labels, + 'dispatch_counts': dispatch_counts, + 'dispatch_amounts': dispatch_amounts, + 'success_counts': success_counts, + 'success_amounts': success_amounts, + } + + # 8. 查询KPI数据 + if granularity == 'day': + try: + stat = DailyDispatchStat.objects.get( + partner_club_id=club_id, + direction=direction, + date=target_date + ) + response_data['summary'] = { + 'dispatch_count': stat.dispatch_count, + 'dispatch_amount': float(stat.dispatch_amount), + 'success_count': stat.success_count, + 'success_amount': float(stat.success_amount), + } + except DailyDispatchStat.DoesNotExist: + response_data['summary'] = { + 'dispatch_count': 0, + 'dispatch_amount': 0.0, + 'success_count': 0, + 'success_amount': 0.0, + } + else: # month + qs_kpi = DailyDispatchStat.objects.filter( + partner_club_id=club_id, + direction=direction, + date__gte=start_date_kpi, + date__lte=end_date_kpi + ) + summary = qs_kpi.aggregate( + dispatch_count=Sum('dispatch_count'), + dispatch_amount=Sum('dispatch_amount'), + success_count=Sum('success_count'), + success_amount=Sum('success_amount') + ) + response_data['summary'] = { + 'dispatch_count': summary['dispatch_count'] or 0, + 'dispatch_amount': float(summary['dispatch_amount'] or 0.0), + 'success_count': summary['success_count'] or 0, + 'success_amount': float(summary['success_amount'] or 0.0), + } + + return Response({'code': 0, 'data': response_data}) + + except Exception as e: + logger.error(f"获取统计数据失败: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + +class PunishDashouView(APIView): + """ + 客服处罚打手接口 + 请求:POST /houtai/kpcf + 参数:{ + "username": "客服手机号", + "dingdan_id": "订单ID", + "reason": "处罚原因(可选)" + } + 认证:JWT,权限码1 + 返回:code=0 + 处罚结果 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + start_time = time.time() + + # 1. 获取参数 + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + reason = request.data.get('reason', '').strip() + + if not username or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + # 2. 身份验证 + current_user = request.user + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权处罚'}) + + # 4. 查询订单并加锁 + with transaction.atomic(): + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + # 5. 订单状态校验(允许处罚的状态:1,2,3,4,5,6,7,8,即除已完成以外的状态?这里宽松处理) + if order.zhuangtai not in [1,2,3,4,5,6,7,8]: + return Response({'code': 400, 'msg': f'订单状态 {order.zhuangtai} 不允许处罚'}) + + # 6. 判断是否已处罚(避免重复) + existing_punish = Chufajilu.objects.filter( + dingdan_id=dingdan_id, + dashouid=order.jiedan_dashou_id + ).exists() + if existing_punish: + return Response({'code': 400, 'msg': '该打手已被处罚过'}) + + # 7. 区分本地处罚还是通知对方平台 + is_cross = order.is_cross + dispatch_type = order.dispatch_type + partner_club_id = order.partner_club_id + partner_order_id = order.partner_order_id + dashou_id = order.jiedan_dashou_id + + # 如果没有接单打手ID,无法处罚 + if not dashou_id: + return Response({'code': 400, 'msg': '订单无接单打手,无法处罚'}) + + # 本地处罚(处罚我方打手) + def local_punish(): + # 扣除打手积分(假设每次扣5分) + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.jifen -= 5 + dashou_profile.save() + + # 创建处罚记录 + Chufajilu.objects.create( + dingdan_id=dingdan_id, + dashouid=dashou_id, + qingqiuid='', # 请求处罚ID,可空 + chuliid=username, + cfliyou=reason or '', + sqzhuangtai=1, # 成功 + bhliyou='', + ssliyou='', + jifen=5, + ) + logger.info(f"本地处罚打手成功: 订单 {dingdan_id}, 打手 {dashou_id}, 扣分5") + + # 情况A:对方派单(dispatch_type=2)或 本地订单 + if dispatch_type == 2 or not is_cross: + # 直接本地处罚 + local_punish() + return Response({'code': 0, 'msg': '处罚成功'}) + + # 情况B:我方派单且有对方信息(需要通知对方平台) + elif dispatch_type == 1 and partner_club_id and partner_order_id: + # 需要通知对方平台处罚(对方平台打手) + # 获取我方俱乐部ID + my_club_config = ClubConfig.objects.filter(is_self=1).first() + if not my_club_config: + logger.error("未找到我方俱乐部配置") + return Response({'code': 500, 'msg': '系统配置错误'}) + self_club_id = my_club_config.club_id + + # 获取对方服务器域名 + try: + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain + except ObjectDoesNotExist: + logger.error(f"未找到合作关系: {self_club_id} <-> {partner_club_id}") + return Response({'code': 500, 'msg': '未找到对方平台配置'}) + + # 调用对方平台处罚接口,最多重试2次 + url = partner_domain.rstrip('/') + '/houtai/kptcfds' + payload = { + 'order_id': partner_order_id, # 对方平台的订单ID + 'club_id': self_club_id, + 'reason': reason, + 'operator': username + } + timeout = 3 + retries = 2 + success = False + for attempt in range(retries): + try: + resp = requests.post(url, json=payload, timeout=timeout) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') == 0: + success = True + logger.info(f"通知对方平台处罚成功: {dingdan_id}") + # 可选:在本平台记录处罚记录(标记已通知对方,但不扣分) + Chufajilu.objects.create( + dingdan_id=dingdan_id, + dashouid=dashou_id, + qingqiuid='', + chuliid=username, + cfliyou=reason or '', + sqzhuangtai=1, + bhliyou='', + ssliyou='', + jifen=0, # 对方扣分,本方不扣 + ) + break + else: + logger.warning(f"对方平台返回错误 (尝试{attempt+1}): {resp_json.get('msg')}") + else: + logger.warning(f"对方平台HTTP错误 (尝试{attempt+1}): {resp.status_code}") + except requests.exceptions.Timeout: + logger.warning(f"请求对方平台超时 (尝试{attempt+1}): {url}") + except Exception as e: + logger.warning(f"请求对方平台异常 (尝试{attempt+1}): {str(e)}") + if not success: + return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'}) + + return Response({'code': 0, 'msg': '处罚成功'}) + + else: + # 其他情况(例如我方派单但缺少对方信息),视为本地订单,处罚我方打手 + local_punish() + return Response({'code': 0, 'msg': '处罚成功'}) + + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {dingdan_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + except Exception as e: + logger.error(f"处罚接口异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + + +@method_decorator(csrf_exempt, name='dispatch') +class PartnerPunishNotifyView(APIView): + """ + 接收对方平台处罚通知接口 + """ + authentication_classes = [] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + try: + order_id = request.data.get('order_id', '').strip() + club_id = request.data.get('club_id', '').strip() + reason = request.data.get('reason', '').strip() + operator = request.data.get('operator', '').strip() + + if not order_id or not club_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 事务开始 + with transaction.atomic(): + # 查询订单并加锁 + try: + order = Dingdan.objects.select_for_update().get(dingdan_id=order_id) + except Dingdan.DoesNotExist: + logger.warning(f"处罚通知:订单不存在 {order_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + + dashou_id = order.jiedan_dashou_id + if not dashou_id: + logger.warning(f"订单 {order_id} 无接单打手,无法处罚") + return Response({'code': 400, 'msg': '订单无接单打手'}) + + # 检查是否已处罚 + existing = Chufajilu.objects.filter( + dingdan_id=order_id, + dashouid=dashou_id + ).exists() + if existing: + logger.info(f"订单 {order_id} 打手 {dashou_id} 已被处罚过,跳过") + return Response({'code': 0, 'msg': 'already punished'}) + + # 扣除积分 + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.jifen -= 5 + dashou_profile.save() + + # 创建处罚记录 + Chufajilu.objects.create( + dingdan_id=order_id, + dashouid=dashou_id, + qingqiuid='', + chuliid=operator, + cfliyou=reason or '', + sqzhuangtai=0, + bhliyou='', + ssliyou='', + jifen=5, + ) + logger.info(f"对方平台处罚成功: 订单 {order_id}, 打手 {dashou_id}") + + return Response({'code': 0, 'msg': 'success'}) + + except Exception as e: + logger.error(f"对方处罚通知接口异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + + + + +# ==================== 接口1:获取俱乐部列表(权限码2) ==================== +class GetClubConfigForPreSettlementView(APIView): + """ + 获取对方俱乐部配置列表(用于预结算页面) + 权限要求:客服身份 + 权限代码2且开启 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + # 1. 校验客服身份 + try: + kefu = UserKefu.objects.select_related('user').get(user=user) + except ObjectDoesNotExist: + return Response({'code': 403, 'msg': '身份错误,非客服账号'}) + if kefu.zhuangtai != 1: + return Response({'code': 403, 'msg': '客服账号已被禁用'}) + + # 2. 获取请求参数中的 username 并校验 + data = request.data + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + # 4. 查询所有对方俱乐部(is_self=0) + club_configs = ClubConfig.objects.filter(is_self=0).only( + 'club_id', 'club_nickname', 'club_avatar', 'storage_bucket_domain' + ) + data_list = [] + for club in club_configs: + data_list.append({ + 'club_id': club.club_id, + 'club_nickname': club.club_nickname or club.club_id, + 'club_avatar': club.club_avatar or '', + 'storage_bucket_domain': club.storage_bucket_domain or '' + }) + return Response({'code': 0, 'msg': 'success', 'data': data_list}) + + except Exception as e: + logger.error(f"获取俱乐部配置失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + +# ==================== 接口2:获取预结算订单列表 ==================== +class GetPreSettlementDataView(APIView): + """获取预结算订单列表(支持筛选、分页、统计)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + # 1. 客服身份校验 + try: + kefu = UserKefu.objects.select_related('user').get(user=user) + except ObjectDoesNotExist: + return Response({'code': 403, 'msg': '身份错误,非客服账号'}) + if kefu.zhuangtai != 1: + return Response({'code': 403, 'msg': '客服账号已被禁用'}) + + data = request.data + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限(003aa) + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权查看俱乐部配置'}) + + # 2. 获取筛选参数 + status_filter = data.get('status_filter') # 0=待处理, 1=已处理(包含已拒绝) + club_id = data.get('club_id') + dingdan_id = data.get('dingdan_id') + partner_order_id = data.get('partner_order_id') + dashou_id = data.get('dashou_id') + clkf = data.get('clkf') + date_str = data.get('date') # 创建日期 YYYY-MM-DD + page = int(data.get('page', 1)) + page_size = int(data.get('pageSize', 20)) + + # 3. 构建查询集 + queryset = PreSettlement.objects.all() + + # 状态筛选:0待处理,1已处理(status=1或2) + if status_filter == 0: + queryset = queryset.filter(status=0) + elif status_filter == 1: + queryset = queryset.filter(status__in=[1, 2]) + # 如果status_filter为其他值(如'all'),则不筛选 + + if club_id: + queryset = queryset.filter(partner_club_id=club_id) + if dingdan_id: + queryset = queryset.filter(dingdan_id__icontains=dingdan_id) + if partner_order_id: + queryset = queryset.filter(partner_order_id__icontains=partner_order_id) + if dashou_id: + queryset = queryset.filter(dashou_id__icontains=dashou_id) + if clkf: + queryset = queryset.filter(operator__icontains=clkf) + if date_str: + try: + target_date = datetime.strptime(date_str, '%Y-%m-%d').date() + queryset = queryset.filter(create_time__date=target_date) + except ValueError: + pass + + # 4. 统计数据(待处理总数量和总金额、已处理总数量) + pending_queryset = PreSettlement.objects.filter(status=0) + # 应用相同的筛选条件(除了状态) + if club_id: + pending_queryset = pending_queryset.filter(partner_club_id=club_id) + if dingdan_id: + pending_queryset = pending_queryset.filter(dingdan_id__icontains=dingdan_id) + if partner_order_id: + pending_queryset = pending_queryset.filter(partner_order_id__icontains=partner_order_id) + if dashou_id: + pending_queryset = pending_queryset.filter(dashou_id__icontains=dashou_id) + if clkf: + pending_queryset = pending_queryset.filter(operator__icontains=clkf) + if date_str: + pending_queryset = pending_queryset.filter(create_time__date=target_date) + + pending_count = pending_queryset.count() + pending_amount = pending_queryset.aggregate(total=Sum('order_amount'))['total'] or Decimal('0.00') + + processed_count = PreSettlement.objects.filter(status__in=[1, 2]) + if club_id: + processed_count = processed_count.filter(partner_club_id=club_id) + if dingdan_id: + processed_count = processed_count.filter(dingdan_id__icontains=dingdan_id) + if partner_order_id: + processed_count = processed_count.filter(partner_order_id__icontains=partner_order_id) + if dashou_id: + processed_count = processed_count.filter(dashou_id__icontains=dashou_id) + if clkf: + processed_count = processed_count.filter(operator__icontains=clkf) + if date_str: + processed_count = processed_count.filter(create_time__date=target_date) + processed_count = processed_count.count() + + # 5. 分页 + total = queryset.count() + start = (page - 1) * page_size + end = start + page_size + items = queryset.order_by('-create_time')[start:end] + + # 6. 构造返回数据 + list_data = [] + for item in items: + list_data.append({ + 'dingdan_id': item.dingdan_id, + 'partner_order_id': item.partner_order_id, + 'partner_club_id': item.partner_club_id, + 'dashou_id': item.dashou_id, + 'amount': float(item.amount), + 'order_amount': float(item.order_amount), + 'operator': item.operator, + 'status': item.status, + 'status_text': dict(PreSettlement.STATUS_CHOICES).get(item.status, '未知'), + 'create_time': item.create_time.strftime('%Y-%m-%d %H:%M:%S') if item.create_time else '', + 'process_time': item.process_time.strftime('%Y-%m-%d %H:%M:%S') if item.process_time else '', + }) + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'list': list_data, + 'total': total, + 'stats': { + 'pending_count': pending_count, + 'pending_amount': float(pending_amount), + 'processed_count': processed_count + } + } + }) + + except Exception as e: + logger.error(f"获取预结算数据失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + +# ==================== 接口3:打款/拒绝操作 ==================== +class PreSettlementActionView(APIView): + """预结算打款/拒绝操作(原子事务,保证资金安全)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + # 1. 客服身份校验 + try: + kefu = UserKefu.objects.select_related('user').get(user=user) + except ObjectDoesNotExist: + return Response({'code': 403, 'msg': '身份错误,非客服账号'}) + if kefu.zhuangtai != 1: + return Response({'code': 403, 'msg': '客服账号已被禁用'}) + + data = request.data + + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 2. 公共权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否有跨平台管理权限() + if '004aa' not in permissions: + return Response({'code': 403, 'msg': '您无权处理预结算打款'}) + + dingdan_id = data.get('dingdan_id') + action = data.get('action') # 'pay' 或 'reject' + + if not dingdan_id or action not in ['pay', 'reject']: + return Response({'code': 400, 'msg': '参数错误'}) + + with transaction.atomic(): + # 锁定预结算记录(行锁) + pre_settlement = PreSettlement.objects.select_for_update().get(dingdan_id=dingdan_id) + + # 只有状态为0(待处理)才能操作 + if pre_settlement.status != 0: + return Response({'code': 400, 'msg': '该记录已处理,不可重复操作'}) + + if action == 'pay': + # 打款操作:增加打手余额及相关统计 + dashou_id = pre_settlement.dashou_id + amount = pre_settlement.amount + + # 查询打手扩展表 + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + except (UserMain.DoesNotExist, ObjectDoesNotExist): + return Response({'code': 404, 'msg': '打手不存在'}) + + # 使用 F 表达式原子更新余额和统计字段(防止并发覆盖) + dashou_profile.yue = F('yue') + amount + dashou_profile.zonge = F('zonge') + amount + dashou_profile.jinrishouyi = F('jinrishouyi') + amount + dashou_profile.jinyueshouyi = F('jinyueshouyi') + amount + dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1 + dashou_profile.save(update_fields=['yue', 'zonge', 'jinrishouyi', 'jinyueshouyi', 'chengjiaozongliang']) + + # 更新预结算状态为已结算 + pre_settlement.status = 1 + pre_settlement.process_time = timezone.now() + pre_settlement.operator = user.phone + pre_settlement.save() + + # ---------- 打手每日统计(成交) ---------- + + try: + dashou_id = pre_settlement.dashou_id + amount = pre_settlement.amount + from houtai.utils import update_dashou_daily_by_action + update_dashou_daily_by_action( + yonghuid=dashou_id, + amount=amount, + action=2 # 2 表示成交(结算) + ) + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + + + logger.info(f"预结算打款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {amount}") + + elif action == 'reject': + # 拒绝操作:仅修改状态,不涉及资金变动 + pre_settlement.status = 2 + pre_settlement.process_time = timezone.now() + pre_settlement.operator = user.phone + pre_settlement.save() + + # ---------- 打手每日统计(成交) ---------- + dashou_id = pre_settlement.dashou_id + amount = pre_settlement.amount + try: + dashou_id = pre_settlement.dashou_id + amount = pre_settlement.amount + from houtai.utils import update_dashou_daily_by_action + update_dashou_daily_by_action( + yonghuid=dashou_id, + amount=amount, + action=3 # 3 表示退款(退款) + ) + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + logger.info(f"预结算拒绝: 订单 {dingdan_id}") + + return Response({'code': 0, 'msg': '操作成功'}) + + except PreSettlement.DoesNotExist: + return Response({'code': 404, 'msg': '预结算记录不存在'}) + except Exception as e: + logger.error(f"预结算操作异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + + +class GetRolePermissionView(APIView): + """ + 获取所有角色、权限以及角色关联的用户数 + 权限要求:用户必须拥有 000001 权限 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 1. 权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 2. 检查是否有 000001 权限 + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + # 3. 获取所有权限 + all_perms = Permission.objects.all().values('perm_code', 'perm_name', 'description') + permissions_list = list(all_perms) + + # 4. 获取所有角色,并统计每个角色下的用户数和关联的权限 + roles = Role.objects.all() + roles_data = [] + for role in roles: + # 统计该角色下的用户数 + user_count = UserRole.objects.filter(role=role).count() + # 获取该角色绑定的权限 + role_perms = RolePermission.objects.filter(role=role).select_related('permission') + perms_of_role = [ + { + 'perm_code': rp.permission.perm_code, + 'perm_name': rp.permission.perm_name, + 'description': rp.permission.description + } + for rp in role_perms + ] + roles_data.append({ + 'role_code': role.role_code, + 'role_name': role.role_name, + 'description': role.description, + 'user_count': user_count, + 'permissions': perms_of_role + }) + + return Response({ + 'code': 0, + 'data': { + 'roles': roles_data, + 'permissions': permissions_list + } + }) + + +class ModifyRolePermissionView(APIView): + """ + 通用接口:支持删除角色、为角色添加权限、移除角色权限 + 操作类型通过 action 字段区分 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + action = request.data.get('action') # delete_role, add_permission, remove_permission + if not username or not action: + return Response({'code': 400, 'msg': '缺少必要参数'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + # 获取角色(需要角色编码) + role_code = request.data.get('role_code') + if not role_code: + return Response({'code': 400, 'msg': '缺少角色编码'}) + + try: + role = Role.objects.get(role_code=role_code) + except Role.DoesNotExist: + return Response({'code': 404, 'msg': '角色不存在'}) + + # 超级管理员角色(role_code='000001')的特殊限制 + if role.role_code == '000001': + if action in ['delete_role']: + return Response({'code': 403, 'msg': '超级管理员角色不能删除'}) + # 修改名称/描述(本接口不涉及名称描述修改,但若后续扩展需限制) + + # 根据 action 处理 + if action == 'delete_role': + # 删除角色:同时删除角色权限关联、用户角色关联 + with transaction.atomic(): + RolePermission.objects.filter(role=role).delete() + UserRole.objects.filter(role=role).delete() + role.delete() + return Response({'code': 0, 'msg': '角色删除成功'}) + + elif action == 'add_permission': + perm_code = request.data.get('perm_code') + if not perm_code: + return Response({'code': 400, 'msg': '缺少权限编码'}) + # 超级管理员角色不能添加 000001 权限(但实际它已拥有,这里防御) + if role.role_code == '000001' and perm_code == '000001': + return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'}) + try: + perm = Permission.objects.get(perm_code=perm_code) + except Permission.DoesNotExist: + return Response({'code': 404, 'msg': '权限不存在'}) + # 检查是否已存在 + if RolePermission.objects.filter(role=role, permission=perm).exists(): + return Response({'code': 400, 'msg': '该角色已拥有此权限'}) + RolePermission.objects.create(role=role, permission=perm) + return Response({'code': 0, 'msg': '权限添加成功'}) + + elif action == 'remove_permission': + perm_code = request.data.get('perm_code') + if not perm_code: + return Response({'code': 400, 'msg': '缺少权限编码'}) + # 超级管理员角色不能删除 000001 权限 + if role.role_code == '000001' and perm_code == '000001': + return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'}) + try: + perm = Permission.objects.get(perm_code=perm_code) + except Permission.DoesNotExist: + return Response({'code': 404, 'msg': '权限不存在'}) + deleted = RolePermission.objects.filter(role=role, permission=perm).delete() + if deleted[0] == 0: + return Response({'code': 400, 'msg': '该角色未拥有此权限'}) + return Response({'code': 0, 'msg': '权限移除成功'}) + + else: + return Response({'code': 400, 'msg': '无效的 action'}) + + +import random +import string +from django.db import transaction + +class AddRoleView(APIView): + """ + 添加角色:自动生成角色编码,前端提供角色名称、描述、选择的权限列表 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + role_name = request.data.get('role_name', '').strip() + description = request.data.get('description', '').strip() + perm_codes = request.data.get('perm_codes', []) # 列表 + + if not username or not role_name: + return Response({'code': 400, 'msg': '缺少必要参数'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + # 检查角色名称是否已存在 + if Role.objects.filter(role_name=role_name).exists(): + return Response({'code': 400, 'msg': '角色名称已存在'}) + + # 生成唯一角色编码(格式:ROLE_ + 8位随机字母数字) + while True: + random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) + role_code = f"ROLE_{random_str}" + if not Role.objects.filter(role_code=role_code).exists(): + break + + # 创建角色 + with transaction.atomic(): + role = Role.objects.create( + role_code=role_code, + role_name=role_name, + description=description, + + ) + # 绑定权限(过滤掉不存在的权限编码) + for pc in perm_codes: + # 不允许添加 000001 权限(此权限仅供超级管理员) + if pc == '000001': + continue + try: + perm = Permission.objects.get(perm_code=pc) + RolePermission.objects.create(role=role, permission=perm) + except Permission.DoesNotExist: + # 忽略不存在的权限,或记录日志 + logger.warning(f"添加角色时忽略不存在的权限编码: {pc}") + pass + + return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}}) + + + + + + +class GetAdminRolesView(APIView): + """获取所有角色(用于筛选和分配)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + roles = Role.objects.all().values('role_code', 'role_name') + return Response({ + 'code': 0, + 'data': { + 'roles': list(roles) + } + }) + + + + +class GetAdminUserListView(APIView): + """获取后台用户列表(支持分页、筛选)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + # 筛选条件 + phone = request.data.get('phone', '').strip() + nicheng = request.data.get('nicheng', '').strip() + role_code = request.data.get('roleCode', '').strip() + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('pageSize', 20)) + + # 基础查询:user_type='kefu' 且存在客服扩展表 + users = UserMain.objects.filter(user_type='kefu', kefu_profile__isnull=False).select_related('kefu_profile') + if phone: + users = users.filter(phone__icontains=phone) + if nicheng: + users = users.filter(kefu_profile__nicheng__icontains=nicheng) + if role_code: + # 筛选拥有该角色的用户 + user_ids_with_role = UserRole.objects.filter(role__role_code=role_code).values_list('account_id', flat=True) + users = users.filter(phone__in=user_ids_with_role) + + # 排序 + users = users.order_by('-create_time') + + # 分页 + paginator = Paginator(users, page_size) + page_obj = paginator.get_page(page) + + # 构建返回数据 + data_list = [] + for user in page_obj: + # 获取用户的所有角色 + user_roles = UserRole.objects.filter(account_id=user.phone).select_related('role') + roles_data = [{'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles] + data_list.append({ + 'phone': user.phone, + 'nicheng': user.kefu_profile.nicheng, + 'roles': roles_data, + 'status': user.kefu_profile.zhuangtai, + 'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S'), + 'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S'), + }) + + return Response({ + 'code': 0, + 'data': { + 'list': data_list, + 'total': paginator.count, + 'page': page, + 'pageSize': page_size + } + }) + + + + +class ModifyAdminUserView(APIView): + """修改后台用户信息(昵称、密码、二级密码、状态、角色)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + action = request.data.get('action') + if not username or not action: + return Response({'code': 400, 'msg': '缺少参数'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + target_phone = request.data.get('phone', '').strip() + if not target_phone: + return Response({'code': 400, 'msg': '缺少目标用户账号'}) + + try: + target_user = UserMain.objects.get(phone=target_phone, user_type='kefu') + target_kefu = target_user.kefu_profile + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + except UserKefu.DoesNotExist: + return Response({'code': 404, 'msg': '用户扩展信息缺失'}) + + with transaction.atomic(): + if action == 'update_user_info': + # 修改昵称、状态、密码、二级密码 + nicheng = request.data.get('nicheng') + status = request.data.get('status') + password = request.data.get('password') + erjimima = request.data.get('erjimima') + if nicheng is not None: + target_kefu.nicheng = nicheng + if status is not None: + target_kefu.zhuangtai = int(status) + if password: + if len(password) < 6: + return Response({'code': 400, 'msg': '密码至少6位'}) + target_user.password = password + if erjimima: + if len(erjimima) < 6: + return Response({'code': 400, 'msg': '二级密码至少6位'}) + target_kefu.erjimima = erjimima # 假设二级密码也需要加密 + target_user.save() + target_kefu.save() + return Response({'code': 0, 'msg': '修改成功'}) + + elif action == 'add_user_roles': + role_codes = request.data.get('role_codes', []) + if not role_codes: + return Response({'code': 400, 'msg': '请选择角色'}) + for rc in role_codes: + try: + role = Role.objects.get(role_code=rc) + except Role.DoesNotExist: + continue + UserRole.objects.get_or_create(account_id=target_phone, role=role) + return Response({'code': 0, 'msg': '角色添加成功'}) + + elif action == 'remove_user_role': + role_code = request.data.get('role_code') + if not role_code: + return Response({'code': 400, 'msg': '缺少角色编码'}) + try: + role = Role.objects.get(role_code=role_code) + except Role.DoesNotExist: + return Response({'code': 404, 'msg': '角色不存在'}) + UserRole.objects.filter(account_id=target_phone, role=role).delete() + return Response({'code': 0, 'msg': '角色移除成功'}) + + else: + return Response({'code': 400, 'msg': '无效的action'}) + + + + + +class AddAdminUserView(APIView): + """添加后台用户(客服账号)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + phone = request.data.get('phone', '').strip() + password = request.data.get('password', '').strip() + erjimima = request.data.get('erjimima', '').strip() + nicheng = request.data.get('nicheng', '').strip() + role_codes = request.data.get('role_codes', []) + + if not all([phone, password, erjimima, nicheng]): + return Response({'code': 400, 'msg': '请填写完整信息'}) + if len(password) < 6 or len(erjimima) < 6: + return Response({'code': 400, 'msg': '密码和二级密码至少6位'}) + + # 校验手机号是否已被占用(且 user_type='kefu') + if UserMain.objects.filter(phone=phone, user_type='kefu').exists(): + return Response({'code': 400, 'msg': '该账号已存在'}) + + # 生成唯一的 yonghuid(7位数字) + while True: + yonghuid = str(random.randint(1000000, 9999999)) + if not UserMain.objects.filter(yonghuid=yonghuid).exists(): + break + + # 生成唯一的 openid(格式:admin_{时间戳}_{随机6位}) + timestamp = str(int(time.time() * 1000)) + random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) + openid = f"admin_{timestamp}_{random_suffix}" + while UserMain.objects.filter(openid=openid).exists(): + random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) + openid = f"admin_{timestamp}_{random_suffix}" + + with transaction.atomic(): + user_main = UserMain.objects.create( + yonghuid=yonghuid, + openid=openid, + phone=phone, + password=password, + user_type='kefu' + ) + # 创建客服扩展表(后台用户扩展) + UserKefu.objects.create( + user=user_main, + nicheng=nicheng, + erjimima=erjimima, + zhuangtai=1 + ) + # 分配角色 + for rc in role_codes: + try: + role = Role.objects.get(role_code=rc) + UserRole.objects.create(account_id=phone, role=role) + except Role.DoesNotExist: + pass + + return Response({'code': 0, 'msg': '添加成功'}) + + + + + + + + +class KefuGetDashouListView(APIView): + """ + 客服获取打手列表接口(仅打手类型) + 需要拥有以下任一权限:001aa,001bb,001cc,001dd,001ee,001ff,001gg + 请求:POST /yonghu/kefuhqdslb + 参数: + username: 客服手机号(用于权限校验) + keyword: 搜索关键词(可选,搜索用户ID或昵称) + page: 页码(从1开始) + page_size: 每页数量 + 返回: + code: 0 + data: { + list: [打手对象], + has_more: bool, + total: int, // 总打手数 + valid_dashou_count: int // 有效打手数(有有效会员) + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数(前端字段为 username) + username = request.data.get('username', '').strip() + keyword = request.data.get('keyword', '').strip() + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + except ValueError: + return Response({'code': 400, 'msg': '分页参数错误'}) + + if not username: + return Response({'code': 401, 'msg': '认证失败'}) + + # 2. 公共权限校验(验证客服身份,获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否拥有打手管理相关权限(至少一个) + required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看打手列表'}) + + # 4. 构建打手查询集(仅打手类型) + dashou_qs = UserDashou.objects.select_related('user').all() + + # 关键词筛选(用户ID或昵称) + if keyword: + dashou_qs = dashou_qs.filter( + Q(user__yonghuid__icontains=keyword) | + Q(nicheng__icontains=keyword) + ) + + # 5. 统计打手总数 + total_dashou = dashou_qs.count() + + # 6. 统计有效打手数(有会员且未过期) + dashou_user_ids = dashou_qs.values_list('user__yonghuid', flat=True) + now = timezone.now() + valid_user_ids = Huiyuangoumai.objects.filter( + yonghu_id__in=dashou_user_ids, + huiyuan_zhuangtai=1, + daoqi_time__gt=now + ).values_list('yonghu_id', flat=True).distinct() + valid_dashou_count = valid_user_ids.count() + + # 7. 分页 + offset = (page - 1) * page_size + dashou_list = dashou_qs.order_by('-create_time')[offset:offset + page_size] + + # 8. 构建返回数据 + result = [] + for dashou in dashou_list: + user = dashou.user + result.append({ + 'yonghuid': user.yonghuid, + 'avatar': user.avatar or '', + 'zaixianzhuangtai': dashou.zaixianzhuangtai, + 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, + 'nicheng': dashou.nicheng or '', + 'zhuangtai': dashou.zhuangtai, + }) + + has_more = (offset + len(dashou_list)) < total_dashou + + return Response({ + 'code': 0, + 'data': { + 'list': result, + 'has_more': has_more, + 'total': total_dashou, + 'valid_dashou_count': valid_dashou_count, + } + }) + + + +class KefuGetDashouDetailView(APIView): + """ + 客服获取打手详情接口 + 请求:POST /yonghu/kefuhqdsxq + 参数:{ + "username": "客服手机号", + "uid": "打手ID (yonghuid)" + } + 认证:JWT + 返回:code=0 + 打手信息 + 会员列表 + 所有会员类型 + 客服权限列表 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('phone', '').strip() + uid = request.data.get('uid', '').strip() + + if not username or not uid: + return Response({'code': 400, 'msg': '缺少参数'}) + + # 2. 公共权限校验(验证客服身份,获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 3. 检查是否拥有打手管理相关权限(至少一个) + required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看打手详情'}) + + # 4. 查询打手主表和扩展表 + try: + user_main = UserMain.objects.select_related('dashou_profile').get( + yonghuid=uid + ) + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '打手不存在'}) + + try: + dashou = user_main.dashou_profile + except ObjectDoesNotExist: + # 用户存在但未注册打手身份,返回基本信息 + user_info = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'zaixianzhuangtai': 0, + 'zhanghaozhuangtai': 0, + 'zhuangtai': 0, + 'nicheng': '', + 'chenghao': '', + 'dianhua': '', + 'wechat': '', + 'phone': user_main.phone or '', + 'create_time': user_main.create_time, + 'ip': user_main.ip or '', + 'jiedanzongliang': 0, + 'chengjiaozongliang': 0, + 'tuikuanliang': 0, + 'jinrijiedan': 0, + 'yue': 0.00, + 'zonge': 0.00, + 'jifen': 0, + 'yajin': 0.00, + 'jinrishouyi': 0.00, + 'jinyueshouyi': 0.00, + 'yaoqingren': '', + 'jieshao': '', + } + member_list = [] + # 仍然需要返回 all_members 和 permissions + all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao')) + return Response({ + 'code': 0, + 'data': { + 'user_info': user_info, + 'member_list': member_list, + 'all_members': all_members, + 'permissions': permissions, + } + }) + + # 5. 构建打手信息 + user_info = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'zaixianzhuangtai': dashou.zaixianzhuangtai, + 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, + 'zhuangtai': dashou.zhuangtai, + 'nicheng': dashou.nicheng or '', + 'chenghao': dashou.chenghao or '', + 'dianhua': dashou.dianhua or '', + 'wechat': dashou.wechat or '', + 'phone': user_main.phone or '', + 'create_time': user_main.create_time, + 'ip': user_main.ip or '', + 'jiedanzongliang': dashou.jiedanzongliang, + 'chengjiaozongliang': dashou.chengjiaozongliang, + 'tuikuanliang': dashou.tuikuanliang, + 'jinrijiedan': dashou.jinrijiedan, + 'yue': float(dashou.yue) if dashou.yue else 0.00, + 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, + 'jifen': dashou.jifen, + 'yajin': float(dashou.yajin) if dashou.yajin else 0.00, + 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00, + 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00, + 'yaoqingren': dashou.yaoqingren or '', + 'jieshao': dashou.jieshao or '', + } + + # 6. 查询打手已拥有的会员(未过期) + member_records = Huiyuangoumai.objects.filter( + yonghu_id=uid + ).order_by('-create_time') + member_list = [] + for record in member_records: + # 检查是否过期 + if hasattr(record, 'jiance_shifou_daoqi'): + is_expired = record.jiance_shifou_daoqi() + else: + is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True + + if not is_expired: + try: + huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id) + huiyuan_name = huiyuan.jieshao + except Huiyuan.DoesNotExist: + huiyuan_name = '未知会员' + member_list.append({ + 'id': record.id, + 'huiyuan_name': huiyuan_name, + 'daoqi_time': record.daoqi_time, + 'is_active': record.huiyuan_zhuangtai == 1, + 'huiyuan_id': record.huiyuan_id, # 用于删除操作 + }) + + # 7. 查询所有会员类型(用于前端添加会员) + all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao')) + + return Response({ + 'code': 0, + 'data': { + 'user_info': user_info, + 'member_list': member_list, + 'all_members': all_members, + 'permissions': permissions, # 返回当前客服的权限列表 + } + }) + + + + +'''class KefuGetDashouDetailView(APIView): + """ + 客服获取打手详情接口 + 请求:POST /yonghu/kefuhqdsxq + 参数:{ + "phone": "客服手机号", + "uid": "打手ID (yonghuid)" + } + 认证:JWT + 返回:code=0 + 打手信息 + 会员列表 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('phone', '').strip() + uid = request.data.get('uid', '').strip() + + if not username or not uid: + return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 客服身份验证 + current_user = request.user + + # 2. 公共权限校验(验证客服身份,获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否拥有打手管理相关权限(至少一个) + required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看打手列表'}) + + # 3. 查询打手主表和扩展表 + try: + # 使用 select_related 预加载 dashou_profile,减少数据库查询 + user_main = UserMain.objects.select_related('dashou_profile').get( + yonghuid=uid + ) + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 验证用户类型是否为打手(可选,但建议验证) + # 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册) + try: + dashou = user_main.dashou_profile + except ObjectDoesNotExist: + # 用户存在但未注册打手身份,返回基本信息 + user_info = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'zaixianzhuangtai': 0, + 'zhanghaozhuangtai': 0, + 'zhuangtai': 0, + 'nicheng': '', + 'chenghao': '', + 'dianhua': '', + 'wechat': '', + 'phone': user_main.phone or '', + 'create_time': user_main.create_time, + 'ip': user_main.ip or '', + 'jiedanzongliang': 0, + 'chengjiaozongliang': 0, + 'tuikuanliang': 0, + 'jinrijiedan': 0, + 'yue': 0.00, + 'zonge': 0.00, + 'jifen': 0, + 'yajin': 0.00, + 'jinrishouyi': 0.00, + 'jinyueshouyi': 0.00, + 'yaoqingren': '', + 'jieshao': '', + } + member_list = [] + return Response({ + 'code': 0, + 'data': { + 'user_info': user_info, + 'member_list': member_list + } + }) + + # 4. 构建打手信息 + user_info = { + 'yonghuid': user_main.yonghuid, + 'avatar': user_main.avatar or '', + 'zaixianzhuangtai': dashou.zaixianzhuangtai, + 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, + 'zhuangtai': dashou.zhuangtai, + 'nicheng': dashou.nicheng or '', + 'chenghao': dashou.chenghao or '', + 'dianhua': dashou.dianhua or '', + 'wechat': dashou.wechat or '', + 'phone': user_main.phone or '', + 'create_time': user_main.create_time, + 'ip': user_main.ip or '', + 'jiedanzongliang': dashou.jiedanzongliang, + 'chengjiaozongliang': dashou.chengjiaozongliang, + 'tuikuanliang': dashou.tuikuanliang, + 'jinrijiedan': dashou.jinrijiedan, + 'yue': float(dashou.yue) if dashou.yue else 0.00, + 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, + 'jifen': dashou.jifen, + 'yajin': float(dashou.yajin) if dashou.yajin else 0.00, + 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00, + 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00, + 'yaoqingren': dashou.yaoqingren or '', + 'jieshao': dashou.jieshao or '', + } + + # 5. 查询打手会员信息(仅未过期) + member_records = Huiyuangoumai.objects.filter( + yonghu_id=uid + ).order_by('-create_time') + + member_list = [] + for record in member_records: + # 调用模型方法检查是否过期 + if hasattr(record, 'jiance_shifou_daoqi'): + is_expired = record.jiance_shifou_daoqi() + else: + # 如果方法不存在,简单判断 + is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True + + if not is_expired: + # 获取会员详情 + try: + huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id) + huiyuan_name = huiyuan.jieshao + except Huiyuan.DoesNotExist: + huiyuan_name = '未知会员' + + member_list.append({ + 'id': record.id, + 'huiyuan_name': huiyuan_name, + 'daoqi_time': record.daoqi_time, + 'is_active': record.huiyuan_zhuangtai == 1, + }) + + return Response({ + 'code': 0, + 'data': { + 'user_info': user_info, + 'member_list': member_list + } + })''' + + + + + + +class KefuUpdateDashouView(APIView): + """ + 客服修改打手信息接口 + 支持操作: + - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb) + - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb) + - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd) + - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee) + - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee) + - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee) + - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg) + - gai_chenghao : 修改打手称号(升级/降级金牌) (权限001hh) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + dashou_id = request.data.get('dashou_id', '').strip() + caozuo = request.data.get('caozuo', '').strip() + value = request.data.get('value') # 金额、积分、天数、称号等 + huiyuan_id = request.data.get('huiyuan_id', '').strip() + + if not username or not dashou_id or not caozuo: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 3. 查询打手 + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '打手不存在'}) + except UserDashou.DoesNotExist: + return Response({'code': 404, 'msg': '打手扩展信息缺失'}) + + with transaction.atomic(): + try: + before = { + 'yue': dashou_profile.yue, + 'yajin': dashou_profile.yajin, + 'jifen': dashou_profile.jifen, + 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai, + 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, + 'zhuangtai': dashou_profile.zhuangtai, + 'chenghao': dashou_profile.chenghao, + } + + # ---------- 余额操作 ---------- + if caozuo == 'jian_yue': + if '001aa' not in permissions: + return Response({'code': 403, 'msg': '无权限减打手余额'}) + amount = Decimal(str(value)) + if dashou_profile.yue < amount: + return Response({'code': 400, 'msg': '余额不足'}) + dashou_profile.yue -= amount + dashou_profile.save() + after = dashou_profile.yue + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + xiugaitijiao=after, + xiugaitijiaoq=before['yue'], + ) + return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after)}}) + + elif caozuo == 'jia_yue': + if '001bb' not in permissions: + return Response({'code': 403, 'msg': '无权限加打手余额'}) + amount = Decimal(str(value)) + dashou_profile.yue += amount + dashou_profile.save() + after = dashou_profile.yue + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + xiugaitijiao=after, + xiugaitijiaoq=before['yue'], + ) + return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after)}}) + + # ---------- 押金操作 ---------- + elif caozuo == 'jian_yajin': + if '001aa' not in permissions: + return Response({'code': 403, 'msg': '无权限减打手押金'}) + amount = Decimal(str(value)) + if dashou_profile.yajin < amount: + return Response({'code': 400, 'msg': '押金不足'}) + dashou_profile.yajin -= amount + dashou_profile.save() + after = dashou_profile.yajin + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + yajin=after, + yyajin=before['yajin'], + ) + return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after)}}) + + elif caozuo == 'jia_yajin': + if '001bb' not in permissions: + return Response({'code': 403, 'msg': '无权限加打手押金'}) + amount = Decimal(str(value)) + dashou_profile.yajin += amount + dashou_profile.save() + after = dashou_profile.yajin + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + yajin=after, + yyajin=before['yajin'], + ) + return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after)}}) + + # ---------- 积分操作 ---------- + elif caozuo == 'jian_jifen': + if '001cc' not in permissions: + return Response({'code': 403, 'msg': '无权限减打手积分'}) + jifen = int(value) + if dashou_profile.jifen < jifen: + return Response({'code': 400, 'msg': '积分不足'}) + dashou_profile.jifen -= jifen + dashou_profile.save() + after = dashou_profile.jifen + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + jifen=after, + yjifen=before['jifen'], + ) + return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after}}) + + elif caozuo == 'jia_jifen': + if '001dd' not in permissions: + return Response({'code': 403, 'msg': '无权限加打手积分'}) + jifen = int(value) + dashou_profile.jifen += jifen + dashou_profile.save() + after = dashou_profile.jifen + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + jifen=after, + yjifen=before['jifen'], + ) + return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after}}) + + # ---------- 状态修改 ---------- + elif caozuo == 'gai_zhanghaozhuangtai': + if '001ee' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手账号状态'}) + new_status = int(value) + if new_status not in [0, 1]: + return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'}) + dashou_profile.zhanghaozhuangtai = new_status + dashou_profile.save() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + qitashuoming=f'修改账号状态为{new_status}', + ) + return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}}) + + elif caozuo == 'gai_zaixianzhuangtai': + if '001ee' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手在线状态'}) + new_status = int(value) + if new_status not in [0, 1]: + return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'}) + dashou_profile.zaixianzhuangtai = new_status + dashou_profile.save() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + qitashuoming=f'修改在线状态为{new_status}', + ) + return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}}) + + elif caozuo == 'gai_zhuangtai': + if '001ee' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手接单状态'}) + new_status = int(value) + if new_status not in [0, 1]: + return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'}) + dashou_profile.zhuangtai = new_status + dashou_profile.save() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + qitashuoming=f'修改接单状态为{new_status}', + ) + return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}}) + + # ---------- 会员操作 ---------- + elif caozuo == 'jia_huiyuan': + if '001ff' not in permissions: + return Response({'code': 403, 'msg': '无权限给打手加会员'}) + if not huiyuan_id: + return Response({'code': 400, 'msg': '缺少会员ID'}) + days = int(value) if value else 30 + try: + huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response({'code': 404, 'msg': '会员类型不存在'}) + record, created = Huiyuangoumai.objects.select_for_update().get_or_create( + yonghu_id=dashou_id, + huiyuan_id=huiyuan_id, + defaults={ + 'huiyuan_zhuangtai': 1, + 'jieshao': huiyuan.jieshao, + 'daoqi_time': timezone.now() + timedelta(days=days), + } + ) + if not created: + new_daoqi = record.daoqi_time + timedelta(days=days) + record.daoqi_time = new_daoqi + record.huiyuan_zhuangtai = 1 + record.save() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + huiyuants=days, + huiyuan_id=huiyuan_id, + qitashuoming=f'加会员 {huiyuan_id},增加{days}天', + ) + return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}}) + + elif caozuo == 'jian_huiyuan': + if '001gg' not in permissions: + return Response({'code': 403, 'msg': '无权限给打手减会员'}) + if not huiyuan_id: + return Response({'code': 400, 'msg': '缺少会员ID'}) + try: + record = Huiyuangoumai.objects.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id) + except Huiyuangoumai.DoesNotExist: + return Response({'code': 404, 'msg': '该打手未购买此会员'}) + record.delete() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + huiyuan_id=huiyuan_id, + qitashuoming=f'移除会员 {huiyuan_id}', + ) + return Response({'code': 0, 'msg': '减会员成功'}) + + # ---------- 称号修改(金牌打手) ---------- + elif caozuo == 'gai_chenghao': + if '001hh' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手称号'}) + new_chenghao = str(value).strip() + if not new_chenghao: + return Response({'code': 400, 'msg': '称号不能为空'}) + # 可限制合法值,这里只记录日志 + dashou_profile.chenghao = new_chenghao + dashou_profile.save() + Xiugaijilu.objects.create( + yonghuid=dashou_id, + xiugaiid=kefu.user.phone, + leixing=2, + qitashuoming=f'修改称号:{before["chenghao"] or "无"} → {new_chenghao}', + ) + return Response({'code': 0, 'msg': '修改称号成功', 'data': {'new_chenghao': new_chenghao}}) + + else: + return Response({'code': 400, 'msg': '未知操作类型'}) + + except Exception as e: + logger.error(f"修改打手信息异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + +class KefuGetShangjiaListView(APIView): + """ + 客服/管理员获取商家列表接口 + 需要拥有以下任一权限:1100a, 1100b + 请求:POST /houtai/hqsjgllb + 参数: + username: 客服手机号(用于权限校验,必填) + keyword: 搜索关键词(可选,搜索商家用户ID或昵称) + balance_op: 余额比较符,gte(>=) / lte(<=)(可选) + balance_value: 余额数值(可选,需与balance_op同时存在) + order_count_op: 派单量比较符,gte(>=) / lte(<=)(可选) + order_count_value: 派单量数值(可选,需与order_count_op同时存在) + status: 商家状态,1=正常,2=禁用(可选) + page: 页码,从1开始,默认1 + page_size: 每页数量,默认20 + 返回: + code: 0 成功,其他失败 + data: { + list: [商家对象], + total: int, // 总商家数 + } + """ + permission_classes = [] # 不依赖全局认证,手动校验 + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + keyword = request.data.get('keyword', '').strip() + balance_op = request.data.get('balance_op', '').strip().lower() + balance_value_str = request.data.get('balance_value') + order_count_op = request.data.get('order_count_op', '').strip().lower() + order_count_value_str = request.data.get('order_count_value') + status_str = request.data.get('status') + + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + if page < 1 or page_size < 1: + raise ValueError + except ValueError: + return Response({'code': 400, 'msg': '分页参数错误'}) + + if not username: + return Response({'code': 401, 'msg': '认证失败,缺少username'}) + + # 2. 公共权限校验(复用现有函数 verify_kefu_permission) + # 假设 verify_kefu_permission 定义在某个公共模块,这里引入 + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查商家管理权限(1100a 或 1100b) + required_perms = {'1100a', '1100b'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看商家列表'}) + + # 4. 构建商家查询集(商家扩展表 + 关联用户主表) + shangjia_qs = UserShangjia.objects.select_related('user').all() + + # 关键词筛选(用户ID或昵称) + if keyword: + shangjia_qs = shangjia_qs.filter( + Q(user__yonghuid__icontains=keyword) | + Q(nicheng__icontains=keyword) + ) + + # 余额筛选 + if balance_op and balance_value_str is not None and balance_value_str != '': + try: + balance_value = Decimal(str(balance_value_str)) + if balance_op == 'gte': + shangjia_qs = shangjia_qs.filter(yue__gte=balance_value) + elif balance_op == 'lte': + shangjia_qs = shangjia_qs.filter(yue__lte=balance_value) + else: + return Response({'code': 400, 'msg': '余额比较符只支持gte或lte'}) + except Exception: + return Response({'code': 400, 'msg': '余额数值格式错误'}) + + # 派单总量筛选(fabu字段) + if order_count_op and order_count_value_str is not None and order_count_value_str != '': + try: + order_count_value = int(order_count_value_str) + if order_count_op == 'gte': + shangjia_qs = shangjia_qs.filter(fabu__gte=order_count_value) + elif order_count_op == 'lte': + shangjia_qs = shangjia_qs.filter(fabu__lte=order_count_value) + else: + return Response({'code': 400, 'msg': '派单量比较符只支持gte或lte'}) + except Exception: + return Response({'code': 400, 'msg': '派单量数值必须为整数'}) + + # 商家状态筛选 + if status_str is not None and status_str != '': + try: + status_val = int(status_str) + if status_val not in [1, 2]: + return Response({'code': 400, 'msg': '商家状态值无效,应为1或2'}) + shangjia_qs = shangjia_qs.filter(zhuangtai=status_val) + except ValueError: + return Response({'code': 400, 'msg': '商家状态格式错误'}) + + # 5. 统计总商家数(应用筛选后) + total = shangjia_qs.count() + + # 6. 分页 + offset = (page - 1) * page_size + shangjia_list = shangjia_qs.order_by('-create_time')[offset:offset + page_size] + + # 7. 构建返回数据 + result = [] + for shangjia in shangjia_list: + user = shangjia.user + result.append({ + 'yonghuid': user.yonghuid, + 'avatar': user.avatar or '', + 'nicheng': shangjia.nicheng or '', + 'yue': str(shangjia.yue), # 转换为字符串,避免前端精度问题 + 'fabu': shangjia.fabu, + 'chengjiao': shangjia.chengjiao, + 'zhuangtai': shangjia.zhuangtai, # 1=正常,2=禁用 + }) + + return Response({ + 'code': 0, + 'data': { + 'list': result, + 'total': total, + } + }) + + + + + + +# yonghu/views.py 中添加 + +class KefuGetShangjiaDetailView(APIView): + """ + 客服/管理员获取商家详情接口 + 需要权限:1100a 或 1100b + 请求:POST /houtai/hthqsjxq + 参数: + username: 客服手机号(用于权限校验,必填) + yonghuid: 商家用户ID(必填) + 返回: + code: 0 成功 + data: 商家所有字段(含主表头像等) + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '认证失败,缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少商家ID'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + required_perms = {'1100a', '1100b'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看商家详情'}) + + # 查询商家 + try: + shangjia = UserShangjia.objects.select_related('user').get(user__yonghuid=yonghuid) + except UserShangjia.DoesNotExist: + return Response({'code': 404, 'msg': '商家不存在'}) + + user = shangjia.user + + data = { + 'yonghuid': user.yonghuid, + 'avatar': user.avatar or '', + 'nicheng': shangjia.nicheng, + 'zhuangtai': shangjia.zhuangtai, # 1正常,2禁用 + 'dianhua': shangjia.dianhua, + 'wechat': shangjia.wechat, + 'yue': str(shangjia.yue), + 'fabu': shangjia.fabu, + 'chengjiao': shangjia.chengjiao, + 'tuikuan': shangjia.tuikuan, + 'jinridingdan': shangjia.jinridingdan, + 'jinriliushui': str(shangjia.jinriliushui), + 'jinyuedingdan': shangjia.jinyuedingdan, + 'jinyueliushui': str(shangjia.jinyueliushui), + 'create_time': shangjia.create_time.isoformat() if shangjia.create_time else None, + } + + return Response({'code': 0, 'data': data}) + + + + + + + +class KefuUpdateShangjiaView(APIView): + """ + 客服/管理员修改商家信息(余额、状态) + POST /houtai/xgsjxx + 参数: + username, yonghuid, action + action可选:add_balance, sub_balance, ban, unban + amount(add_balance/sub_balance时必填) + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + action = request.data.get('action', '').strip().lower() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少商家ID'}) + if action not in ('add_balance', 'sub_balance', 'ban', 'unban'): + return Response({'code': 400, 'msg': 'action参数无效'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 操作权限校验 + if action in ('add_balance', 'ban', 'unban') and '1100a' not in permissions: + return Response({'code': 403, 'msg': '无权限执行此操作(需要1100a)'}) + if action == 'sub_balance' and '1100b' not in permissions: + return Response({'code': 403, 'msg': '无权限减少商家余额(需要1100b)'}) + + # 使用事务,select_for_update 必须在事务内 + with transaction.atomic(): + try: + shangjia = UserShangjia.objects.select_for_update().get(user__yonghuid=yonghuid) + except UserShangjia.DoesNotExist: + return Response({'code': 404, 'msg': '商家不存在'}) + + # 增加余额 + if action == 'add_balance': + amount_str = request.data.get('amount') + try: + amount = Decimal(str(amount_str)) + if amount <= 0 or amount > 1000000: + return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + new_yue = shangjia.yue + amount + if new_yue > 999999999.99: + return Response({'code': 400, 'msg': '增加后余额超出上限'}) + shangjia.yue = new_yue + shangjia.save(update_fields=['yue']) + return Response({'code': 0, 'msg': '余额增加成功', 'data': {'new_balance': str(shangjia.yue)}}) + + # 减少余额 + if action == 'sub_balance': + amount_str = request.data.get('amount') + try: + amount = Decimal(str(amount_str)) + if amount <= 0 or amount > 1000000: + return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + if shangjia.yue < amount: + return Response({'code': 400, 'msg': '余额不足'}) + shangjia.yue = shangjia.yue - amount + shangjia.save(update_fields=['yue']) + return Response({'code': 0, 'msg': '余额减少成功', 'data': {'new_balance': str(shangjia.yue)}}) + + # 封禁 + if action == 'ban': + if shangjia.zhuangtai == 2: + return Response({'code': 400, 'msg': '商家已是封禁状态'}) + shangjia.zhuangtai = 2 + shangjia.save(update_fields=['zhuangtai']) + return Response({'code': 0, 'msg': '商家已封禁', 'data': {'zhuangtai': 2}}) + + # 解封 + if action == 'unban': + if shangjia.zhuangtai == 1: + return Response({'code': 400, 'msg': '商家已是正常状态'}) + shangjia.zhuangtai = 1 + shangjia.save(update_fields=['zhuangtai']) + return Response({'code': 0, 'msg': '商家已解封', 'data': {'zhuangtai': 1}}) + + + + + + + +class GetProductBaseDataView(APIView): + """ + 获取商品基础数据(类型、专区、会员) + 权限:2200a + POST /houtai/htsphqjcsx + 参数:username + 返回: + code:0, data: { + type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...], + zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...], + member_list: [ {huiyuan_id, jieshao, jiage}, ...] + } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取所有商品类型(按 paixu 升序) + type_qs = ShangpinLeixing.objects.all().order_by('paixu') + type_list = [] + for t in type_qs: + # 🔥 修改:统计该类型下的公共商品数量(店铺为空且店铺商品类型为空) + product_count = Shangpin.objects.filter( + leixing_id=t.id, + dianpu__isnull=True, # 所属店铺为空 + dianpu_leixing__isnull=True # 店铺商品类型为空 + ).count() + type_list.append({ + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'paixu': t.paixu, + 'shenhezhuangtai': t.shenhezhuangtai, + 'product_count': product_count, + }) + + # 获取所有专区(按 paixu 升序) + zone_qs = ShangpinZhuanqu.objects.all().order_by('paixu') + zone_list = [] + for z in zone_qs: + # 🔥 修改:统计该专区下的公共商品数量(店铺为空且店铺商品类型为空) + product_count = Shangpin.objects.filter( + zhuanqu_id=z.id, + dianpu__isnull=True, # 所属店铺为空 + dianpu_leixing__isnull=True # 店铺商品类型为空 + ).count() + zone_list.append({ + 'id': z.id, + 'mingzi': z.mingzi or '', + 'leixing_id': z.leixing_id, + 'paixu': z.paixu, + 'shenhezhuangtai': z.shenhezhuangtai, + 'product_count': product_count, + }) + + # 获取所有会员(只需 ID、介绍、价格) + member_list = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage')) + + return Response({ + 'code': 0, + 'data': { + 'type_list': type_list, + 'zone_list': zone_list, + 'member_list': member_list, + } + }) + + + +class GetProductListView(APIView): + """ + 获取商品列表(支持筛选、排序) + 权限:2200a + POST /houtai/hthqsplb + 参数: + username: 必填 + leixing_id: 类型ID(可选) + zhuanqu_id: 专区ID(可选) + keyword: 搜索关键词(商品标题/介绍) + sales_op: 销量比较符 gte/lte + sales_value: 销量数值 + stock_op: 库存比较符 gte/lte + stock_value: 库存数值 + sort_field: 排序字段 zhenshi_xiaoliang / jiage + sort_order: asc/desc + page: 页码(默认1) + page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留) + 返回: + code:0, data: { list: [...], total } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取参数 + leixing_id = request.data.get('leixing_id') + zhuanqu_id = request.data.get('zhuanqu_id') + keyword = request.data.get('keyword', '').strip() + sales_op = request.data.get('sales_op', '').strip().lower() + sales_value = request.data.get('sales_value') + stock_op = request.data.get('stock_op', '').strip().lower() + stock_value = request.data.get('stock_value') + sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage + sort_order = request.data.get('sort_order', 'desc').lower() + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 1000)) + if page < 1 or page_size < 1: + raise ValueError + except: + return Response({'code': 400, 'msg': '分页参数错误'}) + + # 🔥 修改:构建查询集,增加公共商品筛选(店铺为空且店铺商品类型为空) + qs = Shangpin.objects.filter( + dianpu__isnull=True, # 所属店铺为空 + dianpu_leixing__isnull=True # 店铺商品类型为空 + ) + + if leixing_id: + try: + leixing_id = int(leixing_id) + qs = qs.filter(leixing_id=leixing_id) + except: + pass + if zhuanqu_id: + try: + zhuanqu_id = int(zhuanqu_id) + qs = qs.filter(zhuanqu_id=zhuanqu_id) + except: + pass + if keyword: + qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword)) + + # 销量筛选 + if sales_op and sales_value is not None: + try: + sales_val = int(sales_value) + if sales_op == 'gte': + qs = qs.filter(zhenshi_xiaoliang__gte=sales_val) + elif sales_op == 'lte': + qs = qs.filter(zhenshi_xiaoliang__lte=sales_val) + except: + pass + + # 库存筛选 + if stock_op and stock_value is not None: + try: + stock_val = int(stock_value) + if stock_op == 'gte': + qs = qs.filter(kucun__gte=stock_val) + elif stock_op == 'lte': + qs = qs.filter(kucun__lte=stock_val) + except: + pass + + # 排序 + if sort_field in ('zhenshi_xiaoliang', 'jiage'): + if sort_order == 'asc': + qs = qs.order_by(sort_field) + else: + qs = qs.order_by(f'-{sort_field}') + else: + qs = qs.order_by('-create_time') # 默认按创建时间倒序 + + # 分页 + total = qs.count() + offset = (page - 1) * page_size + products = qs[offset:offset + page_size] + + # 构建返回数据 + product_list = [] + for p in products: + product_list.append({ + 'id': p.id, + 'biaoqian': p.biaoqian or '', + 'jiage': str(p.jiage), + 'kucun': p.kucun, + 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, + 'tupian_url': p.tupian_url or '', + 'jieshao': p.jieshao or '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': product_list, + 'total': total, + } + }) + + + + +'''class GetProductBaseDataView(APIView): + """ + 获取商品基础数据(类型、专区、会员) + 权限:2200a + POST /houtai/htsphqjcsx + 参数:username + 返回: + code:0, data: { + type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...], + zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...], + member_list: [ {huiyuan_id, jieshao, jiage}, ...] + } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取所有商品类型(按 paixu 升序) + type_qs = ShangpinLeixing.objects.all().order_by('paixu') + type_list = [] + for t in type_qs: + # 统计该类型下的商品数量(忽略审核状态?通常统计所有商品,可根据需求调整) + product_count = Shangpin.objects.filter(leixing_id=t.id).count() + type_list.append({ + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'paixu': t.paixu, + 'shenhezhuangtai': t.shenhezhuangtai, + 'product_count': product_count, + }) + + # 获取所有专区(按 paixu 升序) + zone_qs = ShangpinZhuanqu.objects.all().order_by('paixu') + zone_list = [] + for z in zone_qs: + product_count = Shangpin.objects.filter(zhuanqu_id=z.id).count() + zone_list.append({ + 'id': z.id, + 'mingzi': z.mingzi or '', + 'leixing_id': z.leixing_id, + 'paixu': z.paixu, + 'shenhezhuangtai': z.shenhezhuangtai, + 'product_count': product_count, + }) + + # 获取所有会员(只需 ID、介绍、价格) + member_list = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage')) + + return Response({ + 'code': 0, + 'data': { + 'type_list': type_list, + 'zone_list': zone_list, + 'member_list': member_list, + } + }) + + +class GetProductListView(APIView): + """ + 获取商品列表(支持筛选、排序) + 权限:2200a + POST /houtai/hthqsplb + 参数: + username: 必填 + leixing_id: 类型ID(可选) + zhuanqu_id: 专区ID(可选) + keyword: 搜索关键词(商品标题/介绍) + sales_op: 销量比较符 gte/lte + sales_value: 销量数值 + stock_op: 库存比较符 gte/lte + stock_value: 库存数值 + sort_field: 排序字段 zhenshi_xiaoliang / jiage + sort_order: asc/desc + page: 页码(默认1) + page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留) + 返回: + code:0, data: { list: [...], total } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取参数 + leixing_id = request.data.get('leixing_id') + zhuanqu_id = request.data.get('zhuanqu_id') + keyword = request.data.get('keyword', '').strip() + sales_op = request.data.get('sales_op', '').strip().lower() + sales_value = request.data.get('sales_value') + stock_op = request.data.get('stock_op', '').strip().lower() + stock_value = request.data.get('stock_value') + sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage + sort_order = request.data.get('sort_order', 'desc').lower() + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 1000)) + if page < 1 or page_size < 1: + raise ValueError + except: + return Response({'code': 400, 'msg': '分页参数错误'}) + + # 构建查询集 + qs = Shangpin.objects.all() + + if leixing_id: + try: + leixing_id = int(leixing_id) + qs = qs.filter(leixing_id=leixing_id) + except: + pass + if zhuanqu_id: + try: + zhuanqu_id = int(zhuanqu_id) + qs = qs.filter(zhuanqu_id=zhuanqu_id) + except: + pass + if keyword: + qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword)) + + # 销量筛选 + if sales_op and sales_value is not None: + try: + sales_val = int(sales_value) + if sales_op == 'gte': + qs = qs.filter(zhenshi_xiaoliang__gte=sales_val) + elif sales_op == 'lte': + qs = qs.filter(zhenshi_xiaoliang__lte=sales_val) + except: + pass + + # 库存筛选 + if stock_op and stock_value is not None: + try: + stock_val = int(stock_value) + if stock_op == 'gte': + qs = qs.filter(kucun__gte=stock_val) + elif stock_op == 'lte': + qs = qs.filter(kucun__lte=stock_val) + except: + pass + + # 排序 + if sort_field in ('zhenshi_xiaoliang', 'jiage'): + if sort_order == 'asc': + qs = qs.order_by(sort_field) + else: + qs = qs.order_by(f'-{sort_field}') + else: + qs = qs.order_by('-create_time') # 默认按创建时间倒序 + + # 分页 + total = qs.count() + offset = (page - 1) * page_size + products = qs[offset:offset + page_size] + + # 构建返回数据 + product_list = [] + for p in products: + product_list.append({ + 'id': p.id, + 'biaoqian': p.biaoqian or '', + 'jiage': str(p.jiage), + 'kucun': p.kucun, + 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, + 'tupian_url': p.tupian_url or '', + 'jieshao': p.jieshao or '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': product_list, + 'total': total, + } + })''' + + +class SaveProductOrderView(APIView): + """ + 保存商品类型或专区的排序 + 权限:2200a + POST /houtai/htsppaixu + 参数: + username: 必填 + type: 'leixing' 或 'zhuanqu' + leixing_id: 当 type='zhuanqu' 时必填(用于限定该类型下的专区) + items: [ {id, paixu}, ... ] # 每个项目的新排序值(从1开始) + 返回: + code:0 成功 + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + sort_type = request.data.get('type', '').strip().lower() + if sort_type not in ('leixing', 'zhuanqu'): + return Response({'code': 400, 'msg': 'type参数必须为leixing或zhuanqu'}) + + items = request.data.get('items') + if not isinstance(items, list) or len(items) == 0: + return Response({'code': 400, 'msg': 'items参数无效'}) + + # 校验每个 item 的格式 + for item in items: + if not isinstance(item, dict) or 'id' not in item or 'paixu' not in item: + return Response({'code': 400, 'msg': 'items内每项需包含id和paixu'}) + try: + item['id'] = int(item['id']) + item['paixu'] = int(item['paixu']) + if item['paixu'] < 1: + return Response({'code': 400, 'msg': 'paixu必须为正整数'}) + except: + return Response({'code': 400, 'msg': 'id或paixu格式错误'}) + + with transaction.atomic(): + if sort_type == 'leixing': + # 更新商品类型的排序 + for item in items: + ShangpinLeixing.objects.filter(id=item['id']).update(paixu=item['paixu']) + else: # zhuanqu + leixing_id = request.data.get('leixing_id') + if not leixing_id: + return Response({'code': 400, 'msg': '更新专区排序时必须提供leixing_id'}) + try: + leixing_id = int(leixing_id) + except: + return Response({'code': 400, 'msg': 'leixing_id格式错误'}) + # 可选:检查该专区是否属于该类型,但不强制,由前端保证 + for item in items: + ShangpinZhuanqu.objects.filter(id=item['id']).update(paixu=item['paixu']) + + return Response({'code': 0, 'msg': '排序保存成功'}) + + +class AddProductView(APIView): + """ + 添加商品(含图片上传) + 权限:2200a + POST /houtai/htfbsp + 参数:multipart/form-data,包含所有商品字段及图片文件 + 字段说明见代码 + """ + permission_classes = [] + parser_classes = [MultiPartParser, FormParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取表单字段 + biaoqian = request.data.get('biaoqian', '').strip() + jiage_str = request.data.get('jiage') + kucun_str = request.data.get('kucun') + duiwai_xiaoliang_str = request.data.get('duiwai_xiaoliang', '0') + leixing_id_str = request.data.get('leixing_id') + zhuanqu_id_str = request.data.get('zhuanqu_id') + yaoqiuleixing_str = request.data.get('yaoqiuleixing', '1') + huiyuan_id = request.data.get('huiyuan_id', '').strip() + yongjin_str = request.data.get('yongjin', '0') + fencheng_type = request.data.get('fencheng_type', 'default') + ewai_dashou_fencheng_str = request.data.get('ewai_dashou_fencheng', '0') + jieshao = request.data.get('jieshao', '').strip() + xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip() + # 图片文件 + image_file = request.FILES.get('file') # 前端上传时字段名为 'file' + + # 必填字段校验 + if not biaoqian: + return Response({'code': 400, 'msg': '商品标题不能为空'}) + try: + jiage = Decimal(jiage_str) + if jiage <= 0: + return Response({'code': 400, 'msg': '价格必须大于0'}) + except: + return Response({'code': 400, 'msg': '价格格式错误'}) + try: + kucun = int(kucun_str) + if kucun < 0: + return Response({'code': 400, 'msg': '库存不能为负数'}) + except: + return Response({'code': 400, 'msg': '库存格式错误'}) + try: + duiwai_xiaoliang = int(duiwai_xiaoliang_str) + except: + duiwai_xiaoliang = 0 + try: + leixing_id = int(leixing_id_str) + except: + return Response({'code': 400, 'msg': '商品类型ID无效'}) + try: + zhuanqu_id = int(zhuanqu_id_str) + except: + return Response({'code': 400, 'msg': '商品专区ID无效'}) + try: + yaoqiuleixing = int(yaoqiuleixing_str) + if yaoqiuleixing not in (1, 2): + raise ValueError + except: + return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'}) + + # 校验类型和专区的存在性及关联性 + try: + leixing_obj = ShangpinLeixing.objects.get(id=leixing_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 400, 'msg': '商品类型不存在'}) + try: + zhuanqu_obj = ShangpinZhuanqu.objects.get(id=zhuanqu_id) + if zhuanqu_obj.leixing_id != leixing_id: + return Response({'code': 400, 'msg': '专区不属于该商品类型'}) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 400, 'msg': '商品专区不存在'}) + + # 处理抢单要求相关字段 + huiyuan_id_val = None + yongjin_val = None + if yaoqiuleixing == 1: + # 会员要求 + if not huiyuan_id: + # 未传递时,尝试从商品类型中获取默认会员ID + huiyuan_id_val = leixing_obj.huiyuan_id + if not huiyuan_id_val: + return Response({'code': 400, 'msg': '未指定会员且商品类型无默认会员'}) + else: + huiyuan_id_val = huiyuan_id + # 验证会员是否存在 + if not Huiyuan.objects.filter(huiyuan_id=huiyuan_id_val).exists(): + return Response({'code': 400, 'msg': '指定的会员不存在'}) + else: + # 佣金要求 + try: + yongjin_val = Decimal(yongjin_str) + if yongjin_val < 0: + return Response({'code': 400, 'msg': '佣金不能为负数'}) + except: + # 若未传递或无效,尝试从商品类型中获取默认佣金 + if leixing_obj.yongjin is not None: + yongjin_val = leixing_obj.yongjin + else: + return Response({'code': 400, 'msg': '佣金格式错误且类型无默认佣金'}) + + # 处理打手分成方式 + kaioi_ewai = False + ewai_fencheng = Decimal('0') + if fencheng_type == 'custom': + try: + ewai_fencheng = Decimal(ewai_dashou_fencheng_str) + if ewai_fencheng < 0: + return Response({'code': 400, 'msg': '自定义分成金额不能为负数'}) + kaioi_ewai = True + except: + return Response({'code': 400, 'msg': '自定义分成金额格式错误'}) + + # 图片上传处理 + tupian_url = '' + if image_file: + # 验证图片 + valid, msg = validate_image(image_file) + if not valid: + return Response({'code': 400, 'msg': f'图片无效:{msg}'}) + # 生成唯一文件名 + ext = image_file.name.split('.')[-1].lower() + if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: + return Response({'code': 400, 'msg': '不支持的图片格式'}) + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + random_str = uuid.uuid4().hex[:8] + filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}" + # 上传到 OSS + uploaded_url = upload_to_oss(image_file, filename) + if not uploaded_url: + return Response({'code': 500, 'msg': '图片上传失败,请稍后重试'}) + tupian_url = filename # 存储相对路径,前端拼接域名 + else: + return Response({'code': 400, 'msg': '商品图片不能为空'}) + + # 创建商品 + try: + with transaction.atomic(): + product = Shangpin.objects.create( + biaoqian=biaoqian, + jiage=jiage, + kucun=kucun, + leixing_id=leixing_id, + zhuanqu_id=zhuanqu_id, + zhenshi_xiaoliang=0, # 初始真实销量为0 + duiwai_xiaoliang=duiwai_xiaoliang, + jieshao=jieshao, + xiadan_xuzhi=xiadan_xuzhi, + tupian_url=tupian_url, + yaoqiuleixing=yaoqiuleixing, + huiyuan_id=huiyuan_id_val, + yongjin=yongjin_val, + kaioi_ewai_dashou_fencheng=kaioi_ewai, + ewai_dashou_fencheng=ewai_fencheng, + paixu=0, # 新商品默认排序最后 + shenhezhuangtai=1, # 正常状态 + ) + except Exception as e: + logger.error(f"创建商品失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '创建商品失败,请稍后重试'}) + + return Response({ + 'code': 0, + 'msg': '商品发布成功', + 'data': { + 'id': product.id, + 'tupian_url': tupian_url, + } + }) + + +# houtai/views_product.py 中添加 + +class DeleteProductView(APIView): + """ + 删除商品(物理删除,同时删除OSS图片) + 权限:2200a + POST /houtai/htscsp + 参数: + username: 客服/管理员账号 + id: 商品ID + 返回: + code:0 成功 + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + product_id = request.data.get('id') + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not product_id: + return Response({'code': 400, 'msg': '缺少商品ID'}) + try: + product_id = int(product_id) + except: + return Response({'code': 400, 'msg': '商品ID格式错误'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 查询商品是否存在 + try: + product = Shangpin.objects.get(id=product_id) + except Shangpin.DoesNotExist: + return Response({'code': 404, 'msg': '商品不存在'}) + + # 先删除OSS上的图片 + tupian_url = product.tupian_url + if tupian_url: + # 调用已有的 delete_from_oss 函数 + from utils.oss_utils import delete_from_oss + delete_success = delete_from_oss(tupian_url) + if not delete_success: + logger.warning(f"删除商品图片失败: {tupian_url}, 商品ID: {product_id}") + # 注意:即使图片删除失败,也应继续删除数据库记录,避免数据不一致 + # 但可以记录日志供后续人工处理 + + # 删除数据库记录 + product.delete() + + logger.info(f"商品删除成功: id={product_id}, 图片路径={tupian_url}, 操作人={username}") + return Response({'code': 0, 'msg': '商品删除成功'}) + + + + + + + +class GetProductDetailView(APIView): + """ + 获取商品详情(包含商品类型、专区、会员列表) + 权限:2200a + POST /houtai/htsphqxq + 参数: + username: 客服/管理员账号 + id: 商品ID + 返回: + code:0, data: { product, type_list, zone_list, member_list } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + product_id = request.data.get('id') + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not product_id: + return Response({'code': 400, 'msg': '缺少商品ID'}) + try: + product_id = int(product_id) + except ValueError: + return Response({'code': 400, 'msg': '商品ID格式错误'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 查询商品 + try: + product = Shangpin.objects.get(id=product_id) + except Shangpin.DoesNotExist: + return Response({'code': 404, 'msg': '商品不存在'}) + + # 查询所有商品类型、专区、会员(用于前端下拉选择) + type_list = list(ShangpinLeixing.objects.all().values( + 'id', 'jieshao', 'tupian_url', 'shenhezhuangtai' + )) + zone_list = list(ShangpinZhuanqu.objects.all().values( + 'id', 'mingzi', 'leixing_id', 'shenhezhuangtai' + )) + member_list = list(Huiyuan.objects.all().values( + 'huiyuan_id', 'jieshao', 'jiage' + )) + + # 构建商品返回数据 + product_data = { + 'id': product.id, + 'biaoqian': product.biaoqian, + 'jiage': str(product.jiage), + 'kucun': product.kucun, + 'leixing_id': product.leixing_id, + 'zhuanqu_id': product.zhuanqu_id, + 'zhenshi_xiaoliang': product.zhenshi_xiaoliang, + 'duiwai_xiaoliang': product.duiwai_xiaoliang, + 'jieshao': product.jieshao, + 'xiadan_xuzhi': product.xiadan_xuzhi, + 'guize_tupian': product.guize_tupian, + 'yaoqiuleixing': product.yaoqiuleixing, + 'huiyuan_id': product.huiyuan_id, + 'yongjin': str(product.yongjin) if product.yongjin is not None else None, + 'tupian_url': product.tupian_url, + 'kaioi_ewai_dashou_fencheng': product.kaioi_ewai_dashou_fencheng, + 'ewai_dashou_fencheng': str(product.ewai_dashou_fencheng) if product.ewai_dashou_fencheng is not None else '0', + 'create_time': product.create_time.isoformat() if product.create_time else None, + 'shenhezhuangtai': product.shenhezhuangtai, + } + + return Response({ + 'code': 0, + 'data': { + 'product': product_data, + 'type_list': type_list, + 'zone_list': zone_list, + 'member_list': member_list, + } + }) + + + +class UpdateProductView(APIView): + """ + 修改商品(支持商品图片和规则图片的更新) + 权限:2200a + POST /houtai/bcspxg + """ + permission_classes = [] + parser_classes = [MultiPartParser, FormParser] + + def post(self, request): + username = request.data.get('username', '').strip() + product_id = request.data.get('id') + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not product_id: + return Response({'code': 400, 'msg': '缺少商品ID'}) + try: + product_id = int(product_id) + except ValueError: + return Response({'code': 400, 'msg': '商品ID格式错误'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 使用事务,将 select_for_update 放在事务内 + with transaction.atomic(): + try: + product = Shangpin.objects.select_for_update().get(id=product_id) + except Shangpin.DoesNotExist: + return Response({'code': 404, 'msg': '商品不存在'}) + + # ---------- 1. 基础字段 ---------- + biaoqian = request.data.get('biaoqian', '').strip() + if not biaoqian: + return Response({'code': 400, 'msg': '商品标题不能为空'}) + + try: + jiage = Decimal(str(request.data.get('jiage', 0))) + if jiage <= 0: + return Response({'code': 400, 'msg': '价格必须大于0'}) + except: + return Response({'code': 400, 'msg': '价格格式错误'}) + + try: + kucun = int(request.data.get('kucun', 0)) + if kucun < 0: + return Response({'code': 400, 'msg': '库存不能为负数'}) + except: + return Response({'code': 400, 'msg': '库存格式错误'}) + + try: + duiwai_xiaoliang = int(request.data.get('duiwai_xiaoliang', 0)) + except: + duiwai_xiaoliang = 0 + + try: + leixing_id = int(request.data.get('leixing_id')) + except: + return Response({'code': 400, 'msg': '商品类型ID无效'}) + + try: + zhuanqu_id = int(request.data.get('zhuanqu_id')) + except: + return Response({'code': 400, 'msg': '商品专区ID无效'}) + + try: + yaoqiuleixing = int(request.data.get('yaoqiuleixing', 1)) + if yaoqiuleixing not in (1, 2): + raise ValueError + except: + return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'}) + + try: + shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) + except: + shenhezhuangtai = 1 + + # 校验类型和专区存在性 + if not ShangpinLeixing.objects.filter(id=leixing_id).exists(): + return Response({'code': 400, 'msg': '商品类型不存在'}) + if not ShangpinZhuanqu.objects.filter(id=zhuanqu_id, leixing_id=leixing_id).exists(): + return Response({'code': 400, 'msg': '专区不存在或不属于该类型'}) + + # ---------- 2. 会员/佣金 ---------- + huiyuan_id = None + yongjin = None + if yaoqiuleixing == 1: + huiyuan_id = request.data.get('huiyuan_id', '').strip() or None + if huiyuan_id and not Huiyuan.objects.filter(huiyuan_id=huiyuan_id).exists(): + return Response({'code': 400, 'msg': '指定的会员不存在'}) + else: + try: + yongjin = Decimal(str(request.data.get('yongjin', 0))) + if yongjin < 0: + return Response({'code': 400, 'msg': '佣金不能为负数'}) + except: + yongjin = Decimal('0') + + # ---------- 3. 打手分成 ---------- + kaioi_ewai = request.data.get('kaioi_ewai_dashou_fencheng') == 'true' + ewai_fencheng = Decimal('0') + if kaioi_ewai: + try: + ewai_fencheng = Decimal(str(request.data.get('ewai_dashou_fencheng', 0))) + if ewai_fencheng < 0: + return Response({'code': 400, 'msg': '分成金额不能为负数'}) + except: + return Response({'code': 400, 'msg': '分成金额格式错误'}) + + jieshao = request.data.get('jieshao', '').strip() + xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip() + + # ---------- 4. 商品图片更新 ---------- + tupian_url = product.tupian_url + tupian_file = request.FILES.get('tupian_file') + if tupian_file: + valid, msg = validate_image(tupian_file) + if not valid: + return Response({'code': 400, 'msg': f'商品图片无效:{msg}'}) + ext = tupian_file.name.split('.')[-1].lower() + if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: + return Response({'code': 400, 'msg': '不支持的图片格式'}) + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + random_str = uuid.uuid4().hex[:8] + new_filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}" + uploaded_url = upload_to_oss(tupian_file, new_filename) + if not uploaded_url: + return Response({'code': 500, 'msg': '商品图片上传失败'}) + if product.tupian_url: + delete_from_oss(product.tupian_url) + tupian_url = new_filename + + # ---------- 5. 规则图片更新 ---------- + guize_tupian = product.guize_tupian + guize_file = request.FILES.get('guize_file') + if guize_file: + valid, msg = validate_image(guize_file) + if not valid: + return Response({'code': 400, 'msg': f'规则图片无效:{msg}'}) + ext = guize_file.name.split('.')[-1].lower() + if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: + return Response({'code': 400, 'msg': '不支持的图片格式'}) + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + random_str = uuid.uuid4().hex[:8] + new_filename = f"shangpin/guizetupian/{timestamp}_{random_str}.{ext}" + uploaded_url = upload_to_oss(guize_file, new_filename) + if not uploaded_url: + return Response({'code': 500, 'msg': '规则图片上传失败'}) + if product.guize_tupian: + delete_from_oss(product.guize_tupian) + guize_tupian = new_filename + + # ---------- 6. 更新数据库 ---------- + product.biaoqian = biaoqian + product.jiage = jiage + product.kucun = kucun + product.duiwai_xiaoliang = duiwai_xiaoliang + product.leixing_id = leixing_id + product.zhuanqu_id = zhuanqu_id + product.yaoqiuleixing = yaoqiuleixing + product.huiyuan_id = huiyuan_id + product.yongjin = yongjin + product.kaioi_ewai_dashou_fencheng = kaioi_ewai + product.ewai_dashou_fencheng = ewai_fencheng + product.jieshao = jieshao + product.xiadan_xuzhi = xiadan_xuzhi + product.tupian_url = tupian_url + product.guize_tupian = guize_tupian + product.shenhezhuangtai = shenhezhuangtai + product.save() + + return Response({'code': 0, 'msg': '商品修改成功', 'data': {'id': product.id}}) + + + + + +class GetMemberListView(APIView): + """ + 获取会员列表 + 权限:3300a + POST /houtai/hthqhylb + 参数:username + 返回:code:0, data: { list: [...] } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '3300a' not in permissions: + return Response({'code': 403, 'msg': '无权限查看会员列表'}) + + members = Huiyuan.objects.all().order_by('-create_time') + member_list = [] + for m in members: + member_list.append({ + 'huiyuan_id': m.huiyuan_id, + 'jieshao': m.jieshao, + 'jtjieshao': m.jtjieshao, + 'jiage': str(m.jiage), + 'guanshifc': str(m.guanshifc), + 'zuzhangfc': str(m.zuzhangfc), + 'goumai_cishu': m.goumai_cishu, + 'create_time': m.create_time.isoformat() if m.create_time else None, + 'update_time': m.update_time.isoformat() if m.update_time else None, + }) + return Response({'code': 0, 'data': {'list': member_list}}) + + +class UpdateMemberView(APIView): + """ + 修改会员信息 + 权限:3300a + POST /houtai/htxghyxx + 参数: + username, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao + 返回:code:0 成功 + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + huiyuan_id = request.data.get('huiyuan_id', '').strip() + jieshao = request.data.get('jieshao', '').strip() + jiage = request.data.get('jiage') + guanshifc = request.data.get('guanshifc') + zuzhangfc = request.data.get('zuzhangfc') + jtjieshao = request.data.get('jtjieshao', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not huiyuan_id: + return Response({'code': 400, 'msg': '缺少会员ID'}) + if not jieshao: + return Response({'code': 400, 'msg': '会员名称不能为空'}) + + try: + jiage = Decimal(str(jiage)) + guanshifc = Decimal(str(guanshifc)) + zuzhangfc = Decimal(str(zuzhangfc)) + if jiage < 0 or guanshifc < 0 or zuzhangfc < 0: + raise ValueError + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + + if guanshifc + zuzhangfc > jiage: + return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '3300a' not in permissions: + return Response({'code': 403, 'msg': '无权限修改会员'}) + + with transaction.atomic(): + try: + member = Huiyuan.objects.select_for_update().get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response({'code': 404, 'msg': '会员不存在'}) + + member.jieshao = jieshao + member.jiage = jiage + member.guanshifc = guanshifc + member.zuzhangfc = zuzhangfc + member.jtjieshao = jtjieshao + member.save() + + return Response({'code': 0, 'msg': '修改成功'}) + + + + +import random +import string + +class AddMemberView(APIView): + """ + 添加会员 + 权限:3300a + POST /houtai/httjhy + 参数: + username, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao + 返回:code:0, data: { huiyuan_id } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + jieshao = request.data.get('jieshao', '').strip() + jiage = request.data.get('jiage') + guanshifc = request.data.get('guanshifc') + zuzhangfc = request.data.get('zuzhangfc') + jtjieshao = request.data.get('jtjieshao', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not jieshao: + return Response({'code': 400, 'msg': '会员名称不能为空'}) + + try: + jiage = Decimal(str(jiage)) + guanshifc = Decimal(str(guanshifc)) + zuzhangfc = Decimal(str(zuzhangfc)) + if jiage <= 0 or guanshifc < 0 or zuzhangfc < 0: + raise ValueError + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + + if guanshifc + zuzhangfc > jiage: + return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '3300a' not in permissions: + return Response({'code': 403, 'msg': '无权限添加会员'}) + + # 生成唯一6位会员ID(数字+字母) + def generate_huiyuan_id(): + while True: + new_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) + if not Huiyuan.objects.filter(huiyuan_id=new_id).exists(): + return new_id + + with transaction.atomic(): + new_id = generate_huiyuan_id() + member = Huiyuan.objects.create( + huiyuan_id=new_id, + jieshao=jieshao, + jiage=jiage, + guanshifc=guanshifc, + zuzhangfc=zuzhangfc, + jtjieshao=jtjieshao, + goumai_cishu=0 + ) + + return Response({'code': 0, 'msg': '添加成功', 'data': {'huiyuan_id': member.huiyuan_id}}) + + + + + +class GetGuanliListView(APIView): + """ + 获取管事列表 + 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一 + POST /houtai/hthqgslb + 参数: + username, keyword(可选), balance_op/balance_value, commission_op/commission_value, + invite_count_op/invite_count_value, status, page, page_size + 返回: + code:0, data: { list: [...], total } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验(需要任一权限) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看管事列表'}) + + # 分页参数 + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + if page < 1 or page_size < 1: + raise ValueError + except: + return Response({'code': 400, 'msg': '分页参数错误'}) + + # 筛选参数 + keyword = request.data.get('keyword', '').strip() + balance_op = request.data.get('balance_op', '').strip().lower() + balance_value = request.data.get('balance_value') + commission_op = request.data.get('commission_op', '').strip().lower() + commission_value = request.data.get('commission_value') + invite_count_op = request.data.get('invite_count_op', '').strip().lower() + invite_count_value = request.data.get('invite_count_value') + status = request.data.get('status') + + # 构建查询:从 UserMain 开始,关联 UserGuanshi 和 UserBoss(左连接获取昵称) + qs = UserMain.objects.filter( + guanshi_profile__isnull=False # 确保是管事 + ).select_related('guanshi_profile').select_related('boss_profile') + + # 关键词搜索(用户ID 或 昵称来自 boss_profile.nickname) + if keyword: + qs = qs.filter( + Q(yonghuid__icontains=keyword) | + Q(boss_profile__nickname__icontains=keyword) + ) + + # 余额筛选(使用 guanshi_profile.yue) + if balance_op and balance_value is not None and balance_value != '': + try: + balance_val = Decimal(str(balance_value)) + if balance_op == 'gte': + qs = qs.filter(guanshi_profile__yue__gte=balance_val) + elif balance_op == 'lte': + qs = qs.filter(guanshi_profile__yue__lte=balance_val) + else: + return Response({'code': 400, 'msg': '余额比较符无效'}) + except: + return Response({'code': 400, 'msg': '余额数值格式错误'}) + + # 分佣总额筛选(guanshi_profile.chongzhifenrun) + if commission_op and commission_value is not None and commission_value != '': + try: + commission_val = Decimal(str(commission_value)) + if commission_op == 'gte': + qs = qs.filter(guanshi_profile__chongzhifenrun__gte=commission_val) + elif commission_op == 'lte': + qs = qs.filter(guanshi_profile__chongzhifenrun__lte=commission_val) + else: + return Response({'code': 400, 'msg': '分佣比较符无效'}) + except: + return Response({'code': 400, 'msg': '分佣数值格式错误'}) + + # 邀请打手总数筛选(guanshi_profile.yaogingshuliang) + if invite_count_op and invite_count_value is not None and invite_count_value != '': + try: + invite_val = int(invite_count_value) + if invite_count_op == 'gte': + qs = qs.filter(guanshi_profile__yaogingshuliang__gte=invite_val) + elif invite_count_op == 'lte': + qs = qs.filter(guanshi_profile__yaogingshuliang__lte=invite_val) + else: + return Response({'code': 400, 'msg': '邀请数量比较符无效'}) + except: + return Response({'code': 400, 'msg': '邀请数量格式错误'}) + + # 状态筛选 + if status is not None and status != '': + try: + status_val = int(status) + if status_val not in (0, 1): + raise ValueError + qs = qs.filter(guanshi_profile__zhuangtai=status_val) + except: + return Response({'code': 400, 'msg': '状态值无效'}) + + # 计算总数 + total = qs.count() + + # 分页 + offset = (page - 1) * page_size + users = qs.order_by('-create_time')[offset:offset + page_size] + + # 构建返回数据 + result = [] + for user in users: + guanshi = user.guanshi_profile + boss = user.boss_profile if hasattr(user, 'boss_profile') else None + result.append({ + 'yonghuid': user.yonghuid, + 'avatar': user.avatar or '', + 'nickname': boss.nickname if boss else '', + 'zhuangtai': guanshi.zhuangtai, + 'yaogingshuliang': guanshi.yaogingshuliang, + 'yue': str(guanshi.yue), + 'chongzhifenrun': str(guanshi.chongzhifenrun), + }) + + return Response({'code': 0, 'data': {'list': result, 'total': total}}) + + + + + + + + +class GetGuanliDetailView(APIView): + """ + 获取管事详情 + 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一 + POST /houtai/hthqgsxq + 参数:username, yonghuid + 返回: + code:0, data: { + base: {...}, + balance: {...}, + permanent_enabled, permanent_amount, + custom_first_dividend: {}, + extra_dividend_map: {}, + all_members: [] + } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + + # 权限校验(需要任一权限) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看管事详情'}) + + # 查询用户主表和管事扩展表 + try: + user = UserMain.objects.select_related('guanshi_profile', 'boss_profile').get(yonghuid=yonghuid) + guanshi = user.guanshi_profile + boss = user.boss_profile if hasattr(user, 'boss_profile') else None + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '管事不存在'}) + except UserGuanshi.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是管事'}) + + # 基础信息 + base_data = { + 'yonghuid': user.yonghuid, + 'nickname': boss.nickname if boss else '', + 'avatar': user.avatar or '', + 'dianhua': guanshi.dianhua or '', + 'wechat': guanshi.wechat or '', + 'zhuangtai': guanshi.zhuangtai, + 'yaoqingma': guanshi.yaoqingma, + 'yaoqingren': guanshi.yaoqingren, + 'invite_qrcode_url': guanshi.invite_qrcode_url or '', + 'yaogingshuliang': guanshi.yaogingshuliang, + 'jinrichongzhi': guanshi.jinrichongzhi, + 'jinyuechongzhi': guanshi.jinyuechongzhi, + 'chongzhifenrun': str(guanshi.chongzhifenrun), + 'create_time': guanshi.create_time.isoformat() if guanshi.create_time else None, + } + + # 余额提现相关 + balance_data = { + 'yue': str(guanshi.yue), + 'jinritixian_jine': str(guanshi.jinritixian_jine), + 'last_tixian_date': guanshi.last_tixian_date.isoformat() if guanshi.last_tixian_date else None, + 'kaioi_ewai_xiane': guanshi.kaioi_ewai_xiane, + 'ewai_xiane': str(guanshi.ewai_xiane), + } + + # 永久分红(二次分红) + permanent_enabled = guanshi.fenghong_erci_enabled + permanent_amount = str(guanshi.fenghong_erci) if guanshi.fenghong_erci else '0' + + # 获取所有会员(用于前端选择) + all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc')) + for m in all_members: + m['jiage'] = str(m['jiage']) + m['guanshifc'] = str(m['guanshifc']) + + # 获取多次分红配置(按次数和会员组织) + # 查询该管事的所有多次分红记录 + duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu') + custom_first = {} # 首次分红定制(cishu=1) + extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } } + + for record in duoci_records: + # 获取会员信息 + member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first() + if not member: + continue + member_info = { + 'jieshao': member.jieshao, + 'jiage': str(member.jiage), + } + if record.cishu == 1: + custom_first[record.huiyuan] = float(record.guanshi_fenhong) + else: + times = record.cishu + if times not in extra_map: + extra_map[times] = {} + extra_map[times][record.huiyuan] = { + 'guanshi_fenhong': float(record.guanshi_fenhong), + **member_info + } + + # 注意:首次分红定制只存储有定制记录的会员,没有定制的会员使用默认值(前端通过会员表展示) + # 额外次数分红按次数返回 + + return Response({ + 'code': 0, + 'data': { + 'base': base_data, + 'balance': balance_data, + 'permanent_enabled': permanent_enabled, + 'permanent_amount': permanent_amount, + 'custom_first_dividend': custom_first, + 'extra_dividend_map': extra_map, + 'all_members': all_members, + } + }) + + + + + +class UpdateGuanliView(APIView): + """ + 修改管事信息(基础信息、余额、提现限额、分红策略) + 权限细分: + - 修改状态(封禁/解封)及昵称/电话/微信:需要4400a + - 增加余额:需要4400c + - 减少余额:需要4400b + - 修改提现限额(kaioi_ewai_xiane/ewai_xiane):需要4400e + - 修改分红相关(永久分红、定制首次、额外次数):需要4400f + POST /houtai/htxggsxx + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + + # 权限校验(先获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 查询管事对象(不在事务外加锁,事务内加锁) + try: + user = UserMain.objects.get(yonghuid=yonghuid) + guanshi = user.guanshi_profile + boss = user.boss_profile if hasattr(user, 'boss_profile') else None + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + except UserGuanshi.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是管事'}) + + # 开始事务,内部使用 select_for_update + with transaction.atomic(): + # 重新获取加锁对象 + user = UserMain.objects.select_for_update().get(yonghuid=yonghuid) + guanshi = user.guanshi_profile + boss = user.boss_profile if hasattr(user, 'boss_profile') else None + + # ---------- 1. 基础信息修改(需要4400a)---------- + nickname = request.data.get('nickname') + dianhua = request.data.get('dianhua') + wechat = request.data.get('wechat') + zhuangtai = request.data.get('zhuangtai') + avatar = request.data.get('avatar') + + if any(v is not None for v in [nickname, dianhua, wechat, zhuangtai, avatar]): + if '4400a' not in permissions: + return Response({'code': 403, 'msg': '无权限修改管事基础信息(需要4400a)'}) + # 处理昵称(在老板扩展表中) + if nickname is not None: + if not boss: + boss = UserBoss.objects.create(user=user, nickname=nickname) + else: + boss.nickname = nickname + boss.save() + # 处理其他字段 + if dianhua is not None: + guanshi.dianhua = dianhua + if wechat is not None: + guanshi.wechat = wechat + if zhuangtai is not None: + try: + zt = int(zhuangtai) + if zt in (0, 1): + guanshi.zhuangtai = zt + except: + pass + if avatar is not None: + user.avatar = avatar + user.save() + guanshi.save() + + # ---------- 2. 余额修改(加:4400c;减:4400b)---------- + new_yue = request.data.get('yue') + if new_yue is not None: + try: + new_yue = Decimal(str(new_yue)) + if new_yue < 0: + return Response({'code': 400, 'msg': '余额不能为负数'}) + except: + return Response({'code': 400, 'msg': '余额格式错误'}) + + old_yue = guanshi.yue + if new_yue > old_yue: + if '4400c' not in permissions: + return Response({'code': 403, 'msg': '无权限增加余额(需要4400c)'}) + elif new_yue < old_yue: + if '4400b' not in permissions: + return Response({'code': 403, 'msg': '无权限减少余额(需要4400b)'}) + guanshi.yue = new_yue + guanshi.save() + + # ---------- 3. 提现限额修改(需要4400e)---------- + kaioi_ewai = request.data.get('kaioi_ewai_xiane') + ewai_val = request.data.get('ewai_xiane') + if kaioi_ewai is not None or ewai_val is not None: + if '4400e' not in permissions: + return Response({'code': 403, 'msg': '无权限修改提现限额(需要4400e)'}) + if kaioi_ewai is not None: + guanshi.kaioi_ewai_xiane = bool(kaioi_ewai) + if ewai_val is not None: + try: + guanshi.ewai_xiane = Decimal(str(ewai_val)) + except: + return Response({'code': 400, 'msg': '额外限额金额格式错误'}) + guanshi.save() + + # ---------- 4. 分红相关修改(需要4400f)---------- + perm_enabled = request.data.get('permanent_enabled') + perm_amount = request.data.get('permanent_amount') + custom_first = request.data.get('custom_first_dividend') + extra_map = request.data.get('extra_dividend_map') + + if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]): + if '4400f' not in permissions: + return Response({'code': 403, 'msg': '无权限修改分红策略(需要4400f)'}) + + # 4.1 永久分红 + if perm_enabled is not None: + guanshi.fenghong_erci_enabled = bool(perm_enabled) + if perm_amount is not None: + try: + guanshi.fenghong_erci = Decimal(str(perm_amount)) + except: + return Response({'code': 400, 'msg': '永久分红金额格式错误'}) + guanshi.save() + + # 4.2 首次定制分红(cishu=1) + if custom_first is not None and isinstance(custom_first, dict): + # 获取当前所有首次定制记录 + existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1) + # 前端传来的定制字典 {会员ID: 金额} + for huiyuan_id, amount in custom_first.items(): + try: + amount = Decimal(str(amount)) + except: + continue + if amount <= 0: + # 金额为0或负数表示删除定制 + DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() + else: + DuociFenhong.objects.update_or_create( + yonghuid=yonghuid, + huiyuan=huiyuan_id, + cishu=1, + defaults={ + 'guanshi_fenhong': amount, + 'zuzhang_fenhong': Decimal('0') + } + ) + # 删除那些在前端字典中不存在的首次定制记录(即已取消定制) + for record in existing_first: + if record.huiyuan not in custom_first: + record.delete() + + # 4.3 额外次数分红(cishu >= 2) + if extra_map is not None and isinstance(extra_map, dict): + # 收集需要保留的记录 + keep_set = set() + for times_str, members in extra_map.items(): + try: + cishu = int(times_str) + if cishu < 2: + continue + except: + continue + if not isinstance(members, dict): + continue + for huiyuan_id, data in members.items(): + if not isinstance(data, dict): + continue + amount = data.get('guanshi_fenhong') + if amount is None: + continue + try: + amount = Decimal(str(amount)) + except: + continue + keep_set.add((cishu, huiyuan_id)) + # 更新或创建 + DuociFenhong.objects.update_or_create( + yonghuid=yonghuid, + huiyuan=huiyuan_id, + cishu=cishu, + defaults={ + 'guanshi_fenhong': amount, + 'zuzhang_fenhong': Decimal('0') + } + ) + # 删除不在 keep_set 中的额外次数记录 + existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2) + for record in existing_extra: + if (record.cishu, record.huiyuan) not in keep_set: + record.delete() + + return Response({'code': 0, 'msg': '修改成功'}) + + + + + + + + + + +class GetWithdrawSettingsView(APIView): + """ + 获取提现设置(手续费利率、每日限额、当日已提现总额) + 权限:需要 5500a 或 5500b 或 5500c 任一 + POST /houtai/hthqtxpz + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + # 需要5500a/b/c任一 + if not any(p in permissions for p in ('5500a', '5500b', '5500c')): + return Response({'code': 403, 'msg': '无权限查看提现设置'}) + + # 1. 手续费利率(Lilubiao:5打手/6管事/8组长/9审核官/10商家/11打手押金) + rate_map = {} + for leixing, code in [(1, '5'), (2, '6'), (3, '8'), (4, '9'), (5, '11'), (6, '10')]: + obj = Lilubiao.objects.filter(fadanpingtai=code).first() + rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0 + + # 1b. 订单/押金分红费率(Lilubiao:1平台订单/3商家订单/12押金管事/13商家订单管事分红) + order_rate_map = {} + for leixing, code in [(1, '1'), (3, '3'), (12, '12'), (13, '13')]: + obj = Lilubiao.objects.filter(fadanpingtai=code).first() + order_rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0 + + # 2. 个人每日限额 (leixing=1,2,3) + quota_map = {} + for leixing in [1, 2, 3]: + obj = TixianQuotaDefault.objects.filter(leixing=leixing).first() + quota_map[leixing] = float(obj.default_quota) if obj else 20.0 + + # 3. 每日总限额(提现类型1~6 → 配置表4~9) + total_limit_map = {} + for withdraw_leixing, quota_leixing in [(1, 4), (2, 5), (3, 6), (4, 7), (5, 8), (6, 9)]: + obj = TixianQuotaDefault.objects.filter(leixing=quota_leixing).first() + total_limit_map[withdraw_leixing] = float(obj.default_quota) if obj else 0.0 + + # 4. 当日已提现总额(按提现类型 leixing 1~6) + today = date.today() + today_totals = {i: 0.0 for i in range(1, 7)} + records = TixianRiTongji.objects.filter(riqi=today) + for rec in records: + if rec.leixing in today_totals: + today_totals[rec.leixing] = float(rec.total_amount) + + # 🆕 获取提现模式(自动1 / 手动2),默认2 + from peizhi.models import WithdrawConfig # 确保导入模型 + config = WithdrawConfig.objects.filter(id=1).first() + withdraw_mode = config.mode if config else 2 + + return Response({ + 'code': 0, + 'data': { + 'withdraw_mode': withdraw_mode, + 'rates': rate_map, + 'order_rates': order_rate_map, + 'quotas': quota_map, + 'total_limits': total_limit_map, + 'today_totals': today_totals, + } + }) + + +class UpdateWithdrawSettingsView(APIView): + """ + 修改提现设置(权限细分): + - 修改打手相关(利率、个人限额、总限额)需要5500a + - 修改管事相关需要5500b + - 修改组长相关需要5500c + 注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算), + 后端不直接修改 TixianRiTongji,该表由提现流程自动累加。 + POST /houtai/htxgtxsz + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 权限映射:要修改的角色需要对应的权限 + # 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3 + rates = request.data.get('rates', {}) + order_rates = request.data.get('order_rates', request.data.get('commission_rates', {})) + quotas = request.data.get('quotas', {}) + total_limits = request.data.get('total_limits', {}) + + # 角色类型映射 + role_perms = {1: '5500a', 2: '5500b', 3: '5500c'} + # 利率代码映射(1-3 有水缸限额;4审核官/5打手押金/6商家余额仅费率) + rate_code_map = {1: '5', 2: '6', 3: '8', 4: '9', 5: '11', 6: '10'} + # 订单/押金分红(1平台订单/3商家订单/12押金管事/13商家订单管事分红) + order_rate_code_map = {1: '1', 3: '3', 12: '12', 13: '13'} + # 总限额:提现类型 → TixianQuotaDefault.leixing(4~9) + total_code_map = {1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9} + + def _rate_key_present(data, role): + return (str(role) in data and data[str(role)] is not None) or \ + (role in data and data[role] is not None) + + with transaction.atomic(): + # 遍历三个角色(限额相关) + for role in [1, 2, 3]: + perm_needed = role_perms[role] + if perm_needed not in permissions: + if _rate_key_present(rates, role) or \ + (str(role) in quotas and quotas[str(role)] is not None) or \ + (role in quotas and quotas.get(role) is not None) or \ + (str(role) in total_limits and total_limits[str(role)] is not None) or \ + (role in total_limits and total_limits.get(role) is not None): + return Response({'code': 403, 'msg': f'无权限修改{["","打手","管事","组长"][role]}相关设置(需要{perm_needed})'}) + + # 其他提现手续费(审核官/打手押金/商家余额):需 5500a/b/c 任一 + extra_role_names = {4: '考核官', 5: '打手押金', 6: '商家余额'} + for role in [4, 5, 6]: + if _rate_key_present(rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')): + return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}提现手续费率'}) + if (str(role) in total_limits and total_limits[str(role)] is not None) or \ + (role in total_limits and total_limits.get(role) is not None): + if not any(p in permissions for p in ('5500a', '5500b', '5500c')): + return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}总限额'}) + + # 订单/押金分红费率:需 5500a/b/c 任一 + for role in [1, 3, 12, 13]: + if _rate_key_present(order_rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')): + order_names = { + 1: '平台订单打手分红', 3: '商家订单打手分红', + 12: '押金管事分红', 13: '管事订单分红', + } + return Response({'code': 403, 'msg': f'无权限修改{order_names[role]}费率'}) + + # 1. 修改手续费利率 + for role in [1, 2, 3, 4, 5, 6]: + val = rates.get(str(role), rates.get(role)) + if val is not None: + rate = Decimal(str(val)) + code = rate_code_map[role] + obj, created = Lilubiao.objects.select_for_update().get_or_create( + fadanpingtai=code, + defaults={'lilu': rate} + ) + if not created: + obj.lilu = rate + obj.save() + + # 1b. 修改订单/押金分红费率 + for role in [1, 3, 12, 13]: + val = order_rates.get(str(role), order_rates.get(role)) + if val is not None: + rate = Decimal(str(val)) + code = order_rate_code_map[role] + obj, created = Lilubiao.objects.select_for_update().get_or_create( + fadanpingtai=code, + defaults={'lilu': rate} + ) + if not created: + obj.lilu = rate + obj.save() + + # 2. 修改个人每日限额 + for role in [1, 2, 3]: + if str(role) in quotas: + val = quotas[str(role)] + if val is not None: + quota = Decimal(str(val)) + obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create( + leixing=role, + defaults={'default_quota': quota} + ) + if not created: + obj.default_quota = quota + obj.save() + + # 3. 修改每日总限额(1打手/2管事/3组长/4考核官/5押金/6商家) + for role in [1, 2, 3, 4, 5, 6]: + val = total_limits.get(str(role), total_limits.get(role)) + if val is not None: + limit = Decimal(str(val)) + code = total_code_map[role] + obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create( + leixing=code, + defaults={'default_quota': limit} + ) + if not created: + obj.default_quota = limit + obj.save() + + # 🆕 修改提现模式(有5500a/b/c任一即可) + from peizhi.models import WithdrawConfig + withdraw_mode = request.data.get('withdraw_mode') + if withdraw_mode is not None: + if not any(p in permissions for p in ('5500a', '5500b', '5500c')): + return Response({'code': 403, 'msg': '无权限修改提现模式'}) + # 更新或创建 id=1 的配置 + WithdrawConfig.objects.update_or_create( + id=1, + defaults={'mode': int(withdraw_mode)} + ) + + # 注意:TixianRiTongji 表由提现流程自动更新,此处不直接修改 + return Response({'code': 0, 'msg': '设置修改成功'}) + + + + + +class GetZuzhangListView(APIView): + """ + 组长列表接口(分页 + 多条件筛选) + 路由:POST /houtai/hthqzzlb + 权限:需要登录,且拥有 6600a~6600e 任一权限 + """ + permission_classes = [IsAuthenticated] + + @transaction.atomic # 保证查询的一致性(可选,对只读查询影响不大但无坏处) + def post(self, request): + # 1. 获取前端传递的 username(用于防越权) + frontend_username = request.data.get('username', '') + + # 2. 调用公共校验方法,验证身份并获取权限列表 + result = verify_kefu_permission(request, username_from_frontend=frontend_username) + + # 重要:verify_kefu_permission 返回格式为 (user_obj, permissions_list) 或 (None, Response) + # 如果第二个元素是 Response 对象,说明校验失败,直接返回该 Response + if isinstance(result, tuple) and len(result) == 2: + user_obj, second = result + if isinstance(second, Response): + return second # 校验失败,直接返回错误响应 + else: + permissions = second # 正常情况:permissions 是列表 + else: + # 防止意外返回格式,视为失败 + return Response({'code': 500, 'msg': '权限校验模块异常'}) + + # 3. 检查是否拥有组长管理所需权限(6600a ~ 6600e 任一) + required_perms = ['6600a', '6600b', '6600c', '6600d', '6600e'] + if not any(perm in permissions for perm in required_perms): + logger.warning(f"用户 {request.user.phone} 尝试访问组长列表但缺少必要权限,已有权限: {permissions}") + return Response({'code': 403, 'msg': '您无权访问组长管理页面'}) + + # 4. 提取请求参数(分页 + 筛选) + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + except ValueError: + page, page_size = 1, 20 + + keyword = request.data.get('keyword', '').strip() + status = request.data.get('status') # 1=正常, 0=禁用 + invite_count_op = request.data.get('invite_count_op') # gte / lte + invite_count_value = request.data.get('invite_count_value') + amount_op = request.data.get('amount_op') + amount_value = request.data.get('amount_value') + commission_op = request.data.get('commission_op') + commission_value = request.data.get('commission_value') + + # 5. 构建基础 QuerySet(预加载关联表) + queryset = UserZuzhang.objects.select_related('user', 'user__boss_profile').all() + + # 6. 应用筛选条件(使用 ORM 安全过滤) + if keyword: + queryset = queryset.filter( + Q(user__yonghuid__icontains=keyword) | + Q(user__boss_profile__nickname__icontains=keyword) + ) + + if status is not None and status != '': + try: + queryset = queryset.filter(zhuangtai=int(status)) + except ValueError: + pass + + # 邀请总数筛选 + if invite_count_value not in (None, ''): + try: + val = float(invite_count_value) + op = invite_count_op if invite_count_op in ('gte', 'lte') else 'gte' + queryset = queryset.filter(**{f'yaoqing_zongshu__{op}': val}) + except (ValueError, TypeError): + pass + + # 可提现金额筛选 + if amount_value not in (None, ''): + try: + val = float(amount_value) + op = amount_op if amount_op in ('gte', 'lte') else 'gte' + queryset = queryset.filter(**{f'ketixian_jine__{op}': val}) + except (ValueError, TypeError): + pass + + # 分佣总额筛选 + if commission_value not in (None, ''): + try: + val = float(commission_value) + op = commission_op if commission_op in ('gte', 'lte') else 'gte' + queryset = queryset.filter(**{f'fenyong_zonge__{op}': val}) + except (ValueError, TypeError): + pass + + # 7. 总数统计 + total = queryset.count() + + # 8. 分页 + offset = (page - 1) * page_size + paged_queryset = queryset[offset:offset + page_size] + + # 9. 构造返回数据 + data_list = [] + for zu in paged_queryset: + user = zu.user + nickname = '' + if hasattr(user, 'boss_profile') and user.boss_profile: + nickname = user.boss_profile.nickname or '' + + data_list.append({ + 'yonghuid': user.yonghuid, + 'avatar': user.avatar or '', + 'nickname': nickname, + 'fenyong_zonge': float(zu.fenyong_zonge), + 'ketixian_jine': float(zu.ketixian_jine), + 'yaoqing_zongshu': zu.yaoqing_zongshu, + 'zhuangtai': zu.zhuangtai, + }) + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'list': data_list, + 'total': total, + } + }) + + + + + + + + + + +class GetZuzhangDetailView(APIView): + """ + 获取组长详情 + 权限:6600a,6600b,6600c,6600d,6600e 任一 + POST /houtai/hthqzzxq + 参数:username, yonghuid + 返回: + code:0, data: { + base: {...}, + balance: {...}, + permanent_enabled, permanent_amount, + custom_first_dividend: {}, + extra_dividend_map: {}, + all_members: [] + } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + + # 权限校验(需要任一权限) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + required_perms = {'6600a', '6600b', '6600c', '6600d', '6600e'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看组长详情'}) + + # 查询组长主表、扩展表、老板扩展表(昵称) + try: + user = UserMain.objects.select_related('zuzhang_profile', 'boss_profile').get(yonghuid=yonghuid) + zuzhang = user.zuzhang_profile + boss = user.boss_profile if hasattr(user, 'boss_profile') else None + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '组长不存在'}) + except UserZuzhang.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是组长'}) + + # 基础信息 + base_data = { + 'yonghuid': user.yonghuid, + 'nickname': boss.nickname if boss else '', + 'avatar': user.avatar or '', + 'dianhua': '', # 组长表无电话字段?组长扩展表没有电话/微信,但前端可能需要,可从UserMain关联?实际上组长表无电话字段,这里留空或从其他表获取 + 'wechat': '', + 'zhuangtai': zuzhang.zhuangtai, + 'yaoqingma': zuzhang.yaoqingma, + 'yaoqingren': '', # 组长表没有邀请人字段 + 'haibao_url': zuzhang.haibao_url or '', + 'yaoqing_zongshu': zuzhang.yaoqing_zongshu, + 'jinri_fenyong': str(zuzhang.jinri_fenyong), + 'jinyue_fenyong': str(zuzhang.jinyue_fenyong), + 'fenyong_zonge': str(zuzhang.fenyong_zonge), + 'create_time': zuzhang.create_time.isoformat() if zuzhang.create_time else None, + } + + # 余额提现相关 + balance_data = { + 'ketixian_jine': str(zuzhang.ketixian_jine), + 'jinri_tixian': str(zuzhang.jinri_tixian), + 'last_tixian_time': zuzhang.last_tixian_time.isoformat() if zuzhang.last_tixian_time else None, + 'kaioi_ewai_tixian': zuzhang.kaioi_ewai_tixian, + 'ewai_tixian_xiane': str(zuzhang.ewai_tixian_xiane), + } + + # 永久分红(额外分红开关和金额) + permanent_enabled = zuzhang.kaioi_ewai_fenhong + permanent_amount = str(zuzhang.ewai_fenhong_jine) if zuzhang.ewai_fenhong_jine else '0' + + # 获取所有会员(用于前端选择) + all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc')) + for m in all_members: + m['jiage'] = str(m['jiage']) + m['zuzhangfc'] = str(m['zuzhangfc']) + + # 获取多次分红配置(按次数和会员组织) + duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu') + custom_first = {} # 首次分红定制(cishu=1) + extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } } + + for record in duoci_records: + # 获取会员信息 + member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first() + if not member: + continue + member_info = { + 'jieshao': member.jieshao, + 'jiage': str(member.jiage), + } + if record.cishu == 1: + custom_first[record.huiyuan] = float(record.zuzhang_fenhong) + else: + times = record.cishu + if times not in extra_map: + extra_map[times] = {} + extra_map[times][record.huiyuan] = { + 'zuzhang_fenhong': float(record.zuzhang_fenhong), + **member_info + } + + return Response({ + 'code': 0, + 'data': { + 'base': base_data, + 'balance': balance_data, + 'permanent_enabled': permanent_enabled, + 'permanent_amount': permanent_amount, + 'custom_first_dividend': custom_first, + 'extra_dividend_map': extra_map, + 'all_members': all_members, + } + }) + + + + +class UpdateZuzhangView(APIView): + """ + 修改组长信息(基础信息、余额、提现限额、分红策略) + 权限细分: + - 增加可提现金额:6600a + - 减少可提现金额:6600b + - 修改状态(封禁/解封):6600c + - 修改提现限额(kaioi_ewai_tixian/ewai_tixian_xiane):6600d + - 修改分红相关(永久分红、定制首次、额外次数):6600e + POST /houtai/htxgzzxx + 参数: + username, yonghuid, + nickname, avatar, dianhua, wechat, zhuangtai, # 基础 + ketixian_jine, kaioi_ewai_tixian, ewai_tixian_xiane, # 余额提现 + permanent_enabled, permanent_amount, # 永久分红 + custom_first_dividend: {}, # 首次定制分红 {会员ID: 金额} + extra_dividend_map: {} # 额外次数分红 {次数: {会员ID: {zuzhang_fenhong}}} + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 查询组长对象 + try: + user = UserMain.objects.get(yonghuid=yonghuid) + zuzhang = user.zuzhang_profile + boss = user.boss_profile if hasattr(user, 'boss_profile') else None + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + except UserZuzhang.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是组长'}) + + # 获取原始数据(用于对比金额变化) + old_ketixian_jine = zuzhang.ketixian_jine + + # 开始事务 + with transaction.atomic(): + # ---------- 1. 基础信息修改(需要6600c?按需求,基础信息修改通常用6600c,但为了安全,暂不校验,如有需要可加)---------- + nickname = request.data.get('nickname', '').strip() + avatar = request.data.get('avatar', '').strip() + dianhua = request.data.get('dianhua', '').strip() + wechat = request.data.get('wechat', '').strip() + zhuangtai = request.data.get('zhuangtai') + # 状态修改需要6600c + if zhuangtai is not None: + if '6600c' not in permissions: + return Response({'code': 403, 'msg': '无权限修改组长状态(需要6600c)'}) + try: + zt = int(zhuangtai) + if zt in (0, 1): + zuzhang.zhuangtai = zt + except: + pass + if nickname: + if not boss: + boss = UserBoss.objects.create(user=user, nickname=nickname) + else: + boss.nickname = nickname + boss.save() + if avatar: + user.avatar = avatar + user.save() + # 组长表没有电话/微信字段,如需存储可暂存到UserMain或忽略 + # 这里不处理 + + # ---------- 2. 可提现金额修改(加:6600a;减:6600b)---------- + new_ketixian_jine = request.data.get('ketixian_jine') + if new_ketixian_jine is not None: + try: + new_val = Decimal(str(new_ketixian_jine)) + if new_val < 0: + return Response({'code': 400, 'msg': '可提现金额不能为负数'}) + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + if new_val > old_ketixian_jine: + if '6600a' not in permissions: + return Response({'code': 403, 'msg': '无权限增加可提现金额(需要6600a)'}) + elif new_val < old_ketixian_jine: + if '6600b' not in permissions: + return Response({'code': 403, 'msg': '无权限减少可提现金额(需要6600b)'}) + zuzhang.ketixian_jine = new_val + zuzhang.save(update_fields=['ketixian_jine']) + + # ---------- 3. 提现限额修改(需要6600d)---------- + kaioi_ewai = request.data.get('kaioi_ewai_tixian') + ewai_val = request.data.get('ewai_tixian_xiane') + if kaioi_ewai is not None or ewai_val is not None: + if '6600d' not in permissions: + return Response({'code': 403, 'msg': '无权限修改提现限额(需要6600d)'}) + if kaioi_ewai is not None: + zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai) + if ewai_val is not None: + try: + zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val)) + except: + return Response({'code': 400, 'msg': '额外限额金额格式错误'}) + zuzhang.save() + + # ---------- 4. 分红相关修改(需要6600e)---------- + perm_enabled = request.data.get('permanent_enabled') + perm_amount = request.data.get('permanent_amount') + custom_first = request.data.get('custom_first_dividend') + extra_map = request.data.get('extra_dividend_map') + + if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]): + if '6600e' not in permissions: + return Response({'code': 403, 'msg': '无权限修改分红策略(需要6600e)'}) + + # 4.1 永久分红(额外分红开关和金额) + if perm_enabled is not None: + zuzhang.kaioi_ewai_fenhong = bool(perm_enabled) + if perm_amount is not None: + try: + zuzhang.ewai_fenhong_jine = Decimal(str(perm_amount)) + except: + return Response({'code': 400, 'msg': '永久分红金额格式错误'}) + zuzhang.save() + + # 4.2 首次定制分红(cishu=1) + if custom_first is not None and isinstance(custom_first, dict): + existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1) + for huiyuan_id, amount in custom_first.items(): + try: + amount = Decimal(str(amount)) + except: + continue + if amount <= 0: + # 删除定制(恢复默认) + DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() + else: + DuociFenhong.objects.update_or_create( + yonghuid=yonghuid, + huiyuan=huiyuan_id, + cishu=1, + defaults={ + 'zuzhang_fenhong': amount, + 'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红 + } + ) + # 删除那些在前端字典中不存在的首次定制记录(即已取消定制) + for record in existing_first: + if record.huiyuan not in custom_first: + record.delete() + + # 4.3 额外次数分红(cishu >= 2) + if extra_map is not None and isinstance(extra_map, dict): + keep_set = set() + for times_str, members in extra_map.items(): + try: + cishu = int(times_str) + if cishu < 2: + continue + except: + continue + if not isinstance(members, dict): + continue + for huiyuan_id, data in members.items(): + if not isinstance(data, dict): + continue + amount = data.get('zuzhang_fenhong') + if amount is None: + continue + try: + amount = Decimal(str(amount)) + except: + continue + keep_set.add((cishu, huiyuan_id)) + DuociFenhong.objects.update_or_create( + yonghuid=yonghuid, + huiyuan=huiyuan_id, + cishu=cishu, + defaults={ + 'zuzhang_fenhong': amount, + 'guanshi_fenhong': Decimal('0') + } + ) + # 删除不在 keep_set 中的额外次数记录 + existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2) + for record in existing_extra: + if (record.cishu, record.huiyuan) not in keep_set: + record.delete() + + # 保存组长其他可能修改的字段(例如电话、微信没有,忽略) + + return Response({'code': 0, 'msg': '修改成功'}) + + + + +class GetProductTypeZoneView(APIView): + """ + 获取商品类型、专区及会员列表 + 权限:需要拥有 7007a + 请求:POST /houtai/hthqsplxzq + 参数:{"username": "客服手机号"} + 返回: + code: 0 + data: { + type_list: [{id, jieshao, tupian_url, yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, is_cross_enabled, paixu, product_count}, ...], + zone_list: [{id, mingzi, leixing_id, shenhezhuangtai, paixu}, ...], + member_list: [{huiyuan_id, jieshao, jiage}, ...] + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '7007a' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + # 查询所有商品类型(含专区数量) + types = ShangpinLeixing.objects.all().order_by('-paixu') # paixu 越大越靠前 + type_list = [] + for t in types: + # 统计该类型下的专区数量 + zone_count = ShangpinZhuanqu.objects.filter(leixing_id=t.id).count() + type_list.append({ + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'yaoqiuleixing': t.yaoqiuleixing, + 'huiyuan_id': t.huiyuan_id or '', + 'yongjin': float(t.yongjin) if t.yongjin is not None else None, + 'shenhezhuangtai': t.shenhezhuangtai, + 'is_cross_enabled': t.is_cross_enabled, + 'is_special_type': t.is_special_type, + 'paixu': t.paixu, + 'product_count': zone_count, + }) + + # 查询所有专区 + zones = ShangpinZhuanqu.objects.all().order_by('-paixu') + zone_list = [ + { + 'id': z.id, + 'mingzi': z.mingzi or '', + 'leixing_id': z.leixing_id, + 'shenhezhuangtai': z.shenhezhuangtai, + 'paixu': z.paixu, + } + for z in zones + ] + + # 查询所有会员(只返回 id、介绍、价格) + members = Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage') + member_list = [ + { + 'huiyuan_id': m['huiyuan_id'], + 'jieshao': m['jieshao'], + 'jiage': float(m['jiage']), + } + for m in members + ] + + return Response({ + 'code': 0, + 'data': { + 'type_list': type_list, + 'zone_list': zone_list, + 'member_list': member_list, + } + }) + + + + + + + + +'''class ModifyProductTypeZoneView(APIView): + """ + 统一修改商品类型/专区(增、删、改) + 权限:需要拥有 7007a + 请求:POST /houtai/htxgsplx + 支持 multipart/form-data 和 application/json + 参数(通用): + username: 客服账号 + type: 'leixing' 或 'zhuanqu' + action: 'add' / 'update' / 'delete' + 类型专用参数: + - add/update: id (update时必填), jieshao, tupian_url(可不上传), file(图片文件), yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, is_cross_enabled, sync_to_products (可选) + - delete: id, delete_products (布尔) + 专区专用参数: + - add/update: id (update时必填), mingzi, leixing_id, shenhezhuangtai + - delete: id, delete_products (布尔) + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser, MultiPartParser, FormParser] + + def post(self, request): + username = request.data.get('username', '').strip() + obj_type = request.data.get('type') # 'leixing' 或 'zhuanqu' + action = request.data.get('action') # 'add', 'update', 'delete' + + if not username or not obj_type or not action: + return Response({'code': 400, 'msg': '缺少必要参数'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '7007a' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + if obj_type == 'leixing': + return self.handle_leixing(request, action) + elif obj_type == 'zhuanqu': + return self.handle_zhuanqu(request, action) + else: + return Response({'code': 400, 'msg': '无效的type参数'}) + + # ---------- 商品类型处理 ---------- + def handle_leixing(self, request, action): + if action == 'add': + return self.add_leixing(request) + elif action == 'update': + return self.update_leixing(request) + elif action == 'delete': + return self.delete_leixing(request) + else: + return Response({'code': 400, 'msg': '无效的action'}) + + def add_leixing(self, request): + # 获取表单字段 + jieshao = request.data.get('jieshao', '').strip() + yaoqiuleixing = request.data.get('yaoqiuleixing') + huiyuan_id = request.data.get('huiyuan_id', '').strip() or None + yongjin = request.data.get('yongjin') + shenhezhuangtai = request.data.get('shenhezhuangtai', 1) + is_cross_enabled = request.data.get('is_cross_enabled', False) + image_file = request.FILES.get('file') # 上传的图片文件 + + if not jieshao: + return Response({'code': 400, 'msg': '类型名称不能为空'}) + if yaoqiuleixing not in [1, 2]: + return Response({'code': 400, 'msg': '抢单要求类型无效'}) + if not image_file: + return Response({'code': 400, 'msg': '请上传类型图片'}) + + # 校验图片 + try: + from utils.oss_utils import validate_image + valid, msg = validate_image(image_file) + if not valid: + return Response({'code': 400, 'msg': msg}) + except ImportError: + pass + + # 上传图片到OSS + ext = image_file.name.split('.')[-1].lower() + new_filename = f"{uuid.uuid4().hex}.{ext}" + oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" + url = upload_to_oss(image_file, oss_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + + # 创建类型 + try: + with transaction.atomic(): + leixing = ShangpinLeixing.objects.create( + jieshao=jieshao, + tupian_url=oss_path, # 存储相对路径 + yaoqiuleixing=yaoqiuleixing, + huiyuan_id=huiyuan_id, + yongjin=Decimal(yongjin) if yongjin else None, + shenhezhuangtai=int(shenhezhuangtai), + is_cross_enabled=is_cross_enabled in [True, 'true', '1'], + paixu=0 # 新添加的类型排序默认0,前端可再调整 + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': leixing.id}}) + except Exception as e: + logger.error(f"添加类型失败: {e}", exc_info=True) + # 删除已上传的图片 + delete_from_oss(oss_path) + return Response({'code': 500, 'msg': '数据库错误'}) + + def update_leixing(self, request): + type_id = request.data.get('id') + if not type_id: + return Response({'code': 400, 'msg': '缺少类型ID'}) + try: + leixing = ShangpinLeixing.objects.get(id=type_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 404, 'msg': '类型不存在'}) + + # 收集修改字段 + changes = {} + jieshao = request.data.get('jieshao') + if jieshao is not None and jieshao != leixing.jieshao: + changes['jieshao'] = jieshao.strip() + + yaoqiuleixing = request.data.get('yaoqiuleixing') + if yaoqiuleixing is not None and int(yaoqiuleixing) != leixing.yaoqiuleixing: + changes['yaoqiuleixing'] = int(yaoqiuleixing) + + huiyuan_id = request.data.get('huiyuan_id', '').strip() or None + if huiyuan_id != leixing.huiyuan_id: + changes['huiyuan_id'] = huiyuan_id + + yongjin = request.data.get('yongjin') + if yongjin is not None: + new_yongjin = Decimal(yongjin) if yongjin else None + if new_yongjin != leixing.yongjin: + changes['yongjin'] = new_yongjin + + shenhezhuangtai = request.data.get('shenhezhuangtai') + if shenhezhuangtai is not None and int(shenhezhuangtai) != leixing.shenhezhuangtai: + changes['shenhezhuangtai'] = int(shenhezhuangtai) + + is_cross = request.data.get('is_cross_enabled') + if is_cross is not None: + new_cross = is_cross in [True, 'true', '1'] + if new_cross != leixing.is_cross_enabled: + changes['is_cross_enabled'] = new_cross + + # 图片处理 + image_file = request.FILES.get('file') + old_image_path = leixing.tupian_url + if image_file: + # 验证并上传新图片 + try: + from utils.oss_utils import validate_image + valid, msg = validate_image(image_file) + if not valid: + return Response({'code': 400, 'msg': msg}) + except ImportError: + pass + ext = image_file.name.split('.')[-1].lower() + new_filename = f"{uuid.uuid4().hex}.{ext}" + oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" + url = upload_to_oss(image_file, oss_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + changes['tupian_url'] = oss_path + + if not changes and not image_file: + return Response({'code': 400, 'msg': '未做任何修改'}) + + # 执行更新 + sync_to_products = request.data.get('sync_to_products') in [True, 'true', '1'] + try: + with transaction.atomic(): + # 更新类型 + for field, value in changes.items(): + setattr(leixing, field, value) + leixing.save() + + # 如果修改了抢单要求相关字段且需要同步商品 + if sync_to_products and ('yaoqiuleixing' in changes or 'huiyuan_id' in changes or 'yongjin' in changes): + # 更新所有关联此类型的商品 + product_updates = {} + if 'yaoqiuleixing' in changes: + product_updates['yaoqiuleixing'] = changes['yaoqiuleixing'] + if 'huiyuan_id' in changes: + product_updates['huiyuan_id'] = changes['huiyuan_id'] + if 'yongjin' in changes: + product_updates['yongjin'] = changes['yongjin'] + Shangpin.objects.filter(leixing_id=type_id).update(**product_updates) + + # 删除旧图片(新图片上传成功后才删除) + if image_file and old_image_path: + delete_from_oss(old_image_path) + + return Response({'code': 0, 'msg': '修改成功'}) + except Exception as e: + logger.error(f"更新类型失败: {e}", exc_info=True) + # 如果上传了新图片,尝试删除 + if image_file and 'oss_path' in locals(): + delete_from_oss(oss_path) + return Response({'code': 500, 'msg': '修改失败'}) + + def delete_leixing(self, request): + type_id = request.data.get('id') + if not type_id: + return Response({'code': 400, 'msg': '缺少类型ID'}) + delete_products = request.data.get('delete_products') in [True, 'true', '1'] + + try: + leixing = ShangpinLeixing.objects.get(id=type_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 404, 'msg': '类型不存在'}) + + with transaction.atomic(): + # 删除该类型下的所有专区 + zones = ShangpinZhuanqu.objects.filter(leixing_id=type_id) + if delete_products: + # 删除专区下的所有商品 + for zone in zones: + Shangpin.objects.filter(zhuanqu_id=zone.id).delete() + # 删除所有专区 + zones.delete() + # 删除该类型下的所有商品(可能还有直接关联类型但无专区的商品) + Shangpin.objects.filter(leixing_id=type_id).delete() + else: + # 只删除专区,不删除商品 + zones.delete() + # 商品保持原样(leixing_id 保留,但类型已删除,可能导致数据不一致,按需求不处理) + # 也可以将商品的 leixing_id 置空 + Shangpin.objects.filter(leixing_id=type_id).update(leixing_id=None) + + # 删除类型本身 + leixing.delete() + + # 删除类型图片 + if leixing.tupian_url: + delete_from_oss(leixing.tupian_url) + + return Response({'code': 0, 'msg': '删除成功'}) + + # ---------- 商品专区处理 ---------- + def handle_zhuanqu(self, request, action): + if action == 'add': + return self.add_zhuanqu(request) + elif action == 'update': + return self.update_zhuanqu(request) + elif action == 'delete': + return self.delete_zhuanqu(request) + else: + return Response({'code': 400, 'msg': '无效的action'}) + + def add_zhuanqu(self, request): + mingzi = request.data.get('mingzi', '').strip() + leixing_id = request.data.get('leixing_id') + shenhezhuangtai = request.data.get('shenhezhuangtai', 1) + + if not mingzi: + return Response({'code': 400, 'msg': '专区名称不能为空'}) + if not leixing_id: + return Response({'code': 400, 'msg': '请选择所属类型'}) + # 校验类型是否存在 + if not ShangpinLeixing.objects.filter(id=leixing_id).exists(): + return Response({'code': 404, 'msg': '所属类型不存在'}) + + try: + with transaction.atomic(): + zhuanqu = ShangpinZhuanqu.objects.create( + mingzi=mingzi, + leixing_id=int(leixing_id), + shenhezhuangtai=int(shenhezhuangtai), + paixu=0 + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': zhuanqu.id}}) + except Exception as e: + logger.error(f"添加专区失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '数据库错误'}) + + def update_zhuanqu(self, request): + zone_id = request.data.get('id') + if not zone_id: + return Response({'code': 400, 'msg': '缺少专区ID'}) + try: + zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 404, 'msg': '专区不存在'}) + + changes = {} + mingzi = request.data.get('mingzi') + if mingzi is not None and mingzi != zhuanqu.mingzi: + changes['mingzi'] = mingzi.strip() + leixing_id = request.data.get('leixing_id') + if leixing_id is not None and int(leixing_id) != zhuanqu.leixing_id: + # 校验新类型存在 + if not ShangpinLeixing.objects.filter(id=leixing_id).exists(): + return Response({'code': 404, 'msg': '新所属类型不存在'}) + changes['leixing_id'] = int(leixing_id) + shenhezhuangtai = request.data.get('shenhezhuangtai') + if shenhezhuangtai is not None and int(shenhezhuangtai) != zhuanqu.shenhezhuangtai: + changes['shenhezhuangtai'] = int(shenhezhuangtai) + + if not changes: + return Response({'code': 400, 'msg': '未做任何修改'}) + + try: + with transaction.atomic(): + for field, value in changes.items(): + setattr(zhuanqu, field, value) + zhuanqu.save() + return Response({'code': 0, 'msg': '修改成功'}) + except Exception as e: + logger.error(f"更新专区失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '修改失败'}) + + def delete_zhuanqu(self, request): + zone_id = request.data.get('id') + if not zone_id: + return Response({'code': 400, 'msg': '缺少专区ID'}) + delete_products = request.data.get('delete_products') in [True, 'true', '1'] + + try: + zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 404, 'msg': '专区不存在'}) + + with transaction.atomic(): + if delete_products: + Shangpin.objects.filter(zhuanqu_id=zone_id).delete() + else: + Shangpin.objects.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None) + zhuanqu.delete() + + return Response({'code': 0, 'msg': '删除成功'})''' + +class ModifyProductTypeZoneView(APIView): + """ + 统一修改商品类型/专区(增、删、改) + 权限:需要拥有 7007a + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser, MultiPartParser, FormParser] + + def post(self, request): + username = request.data.get('username', '').strip() + obj_type = request.data.get('type') # 'leixing' 或 'zhuanqu' + action = request.data.get('action') # 'add', 'update', 'delete' + + if not username or not obj_type or not action: + return Response({'code': 400, 'msg': '缺少必要参数'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '7007a' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + if obj_type == 'leixing': + return self.handle_leixing(request, action) + elif obj_type == 'zhuanqu': + return self.handle_zhuanqu(request, action) + else: + return Response({'code': 400, 'msg': '无效的type参数'}) + + # ---------- 商品类型处理 ---------- + def handle_leixing(self, request, action): + if action == 'add': + return self.add_leixing(request) + elif action == 'update': + return self.update_leixing(request) + elif action == 'delete': + return self.delete_leixing(request) + else: + return Response({'code': 400, 'msg': '无效的action'}) + + def add_leixing(self, request): + # 获取并校验字段 + jieshao = request.data.get('jieshao', '').strip() + if not jieshao: + return Response({'code': 400, 'msg': '类型名称不能为空'}) + + # 强制转为整数 + try: + yaoqiuleixing = int(request.data.get('yaoqiuleixing', 0)) + if yaoqiuleixing not in [1, 2]: + raise ValueError + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'}) + + try: + shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) + if shenhezhuangtai not in [1, 2]: + raise ValueError + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) + + is_cross_enabled = request.data.get('is_cross_enabled', False) + is_cross_enabled = is_cross_enabled in [True, 'true', '1', 'on'] + is_special_type = request.data.get('is_special_type', False) + is_special_type = is_special_type in [True, 'true', '1', 'on'] + + huiyuan_id = request.data.get('huiyuan_id', '').strip() or None + yongjin = request.data.get('yongjin') + if yongjin is not None and yongjin != '': + try: + yongjin = Decimal(str(yongjin)) + except: + return Response({'code': 400, 'msg': '佣金金额格式错误'}) + else: + yongjin = None + + # 图片校验 + image_file = request.FILES.get('file') + if not image_file: + return Response({'code': 400, 'msg': '请上传类型图片'}) + + # 验证图片 + try: + from utils.oss_utils import validate_image + valid, msg = validate_image(image_file) + if not valid: + return Response({'code': 400, 'msg': msg}) + except ImportError: + pass + + # 上传图片 + ext = image_file.name.split('.')[-1].lower() + new_filename = f"{uuid.uuid4().hex}.{ext}" + oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" + url = upload_to_oss(image_file, oss_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + + # 创建类型 + try: + with transaction.atomic(): + leixing = ShangpinLeixing.objects.create( + jieshao=jieshao, + tupian_url=oss_path, + yaoqiuleixing=yaoqiuleixing, + huiyuan_id=huiyuan_id, + yongjin=yongjin, + shenhezhuangtai=shenhezhuangtai, + is_cross_enabled=is_cross_enabled, + is_special_type=is_special_type, + paixu=0 + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': leixing.id}}) + except Exception as e: + logger.error(f"添加类型失败: {e}", exc_info=True) + delete_from_oss(oss_path) + return Response({'code': 500, 'msg': '数据库错误'}) + + def update_leixing(self, request): + type_id = request.data.get('id') + if not type_id: + return Response({'code': 400, 'msg': '缺少类型ID'}) + try: + leixing = ShangpinLeixing.objects.get(id=type_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 404, 'msg': '类型不存在'}) + + changes = {} + # 名称 + jieshao = request.data.get('jieshao') + if jieshao is not None and jieshao.strip() != leixing.jieshao: + changes['jieshao'] = jieshao.strip() + + # 抢单要求类型(强制转整数) + yaoqiuleixing = request.data.get('yaoqiuleixing') + if yaoqiuleixing is not None: + try: + new_val = int(yaoqiuleixing) + if new_val not in [1, 2]: + raise ValueError + if new_val != leixing.yaoqiuleixing: + changes['yaoqiuleixing'] = new_val + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'}) + + # 会员ID + huiyuan_id = request.data.get('huiyuan_id', '').strip() or None + if huiyuan_id != leixing.huiyuan_id: + changes['huiyuan_id'] = huiyuan_id + + # 佣金金额 + yongjin = request.data.get('yongjin') + if yongjin is not None: + if yongjin == '': + new_val = None + else: + try: + new_val = Decimal(str(yongjin)) + except: + return Response({'code': 400, 'msg': '佣金金额格式错误'}) + if new_val != leixing.yongjin: + changes['yongjin'] = new_val + + # 审核状态 + shenhezhuangtai = request.data.get('shenhezhuangtai') + if shenhezhuangtai is not None: + try: + new_val = int(shenhezhuangtai) + if new_val not in [1, 2,3]: + raise ValueError + if new_val != leixing.shenhezhuangtai: + changes['shenhezhuangtai'] = new_val + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) + + # 跨平台互通 + is_cross = request.data.get('is_cross_enabled') + if is_cross is not None: + new_cross = is_cross in [True, 'true', '1', 'on'] + if new_cross != leixing.is_cross_enabled: + changes['is_cross_enabled'] = new_cross + + # 吃龙虾特殊类型 + is_special = request.data.get('is_special_type') + if is_special is not None: + new_special = is_special in [True, 'true', '1', 'on'] + if new_special != leixing.is_special_type: + changes['is_special_type'] = new_special + + # 图片处理 + image_file = request.FILES.get('file') + old_image_path = leixing.tupian_url + if image_file: + try: + from utils.oss_utils import validate_image + valid, msg = validate_image(image_file) + if not valid: + return Response({'code': 400, 'msg': msg}) + except ImportError: + pass + ext = image_file.name.split('.')[-1].lower() + new_filename = f"{uuid.uuid4().hex}.{ext}" + oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" + url = upload_to_oss(image_file, oss_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + changes['tupian_url'] = oss_path + + if not changes and not image_file: + return Response({'code': 400, 'msg': '未做任何修改'}) + + sync_to_products = request.data.get('sync_to_products') in [True, 'true', '1', 'on'] + try: + with transaction.atomic(): + for field, value in changes.items(): + setattr(leixing, field, value) + leixing.save() + + # 同步商品 + if sync_to_products and ('yaoqiuleixing' in changes or 'huiyuan_id' in changes or 'yongjin' in changes): + product_updates = {} + if 'yaoqiuleixing' in changes: + product_updates['yaoqiuleixing'] = changes['yaoqiuleixing'] + if 'huiyuan_id' in changes: + product_updates['huiyuan_id'] = changes['huiyuan_id'] + if 'yongjin' in changes: + product_updates['yongjin'] = changes['yongjin'] + if product_updates: + Shangpin.objects.filter(leixing_id=type_id).update(**product_updates) + + # 删除旧图片 + if image_file and old_image_path: + delete_from_oss(old_image_path) + + return Response({'code': 0, 'msg': '修改成功'}) + except Exception as e: + logger.error(f"更新类型失败: {e}", exc_info=True) + if image_file and 'oss_path' in locals(): + delete_from_oss(oss_path) + return Response({'code': 500, 'msg': '修改失败'}) + + def delete_leixing(self, request): + type_id = request.data.get('id') + if not type_id: + return Response({'code': 400, 'msg': '缺少类型ID'}) + delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on'] + + try: + leixing = ShangpinLeixing.objects.get(id=type_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 404, 'msg': '类型不存在'}) + + with transaction.atomic(): + # 删除该类型下的所有专区 + zones = ShangpinZhuanqu.objects.filter(leixing_id=type_id) + if delete_products: + # 删除专区下的所有商品 + for zone in zones: + Shangpin.objects.filter(zhuanqu_id=zone.id).delete() + zones.delete() + # 删除直接关联该类型的商品 + Shangpin.objects.filter(leixing_id=type_id).delete() + else: + zones.delete() + # 仅清除商品的类型关联 + Shangpin.objects.filter(leixing_id=type_id).update(leixing_id=None) + + leixing.delete() + + if leixing.tupian_url: + delete_from_oss(leixing.tupian_url) + + return Response({'code': 0, 'msg': '删除成功'}) + + # ---------- 商品专区处理 ---------- + def handle_zhuanqu(self, request, action): + if action == 'add': + return self.add_zhuanqu(request) + elif action == 'update': + return self.update_zhuanqu(request) + elif action == 'delete': + return self.delete_zhuanqu(request) + else: + return Response({'code': 400, 'msg': '无效的action'}) + + def add_zhuanqu(self, request): + mingzi = request.data.get('mingzi', '').strip() + leixing_id = request.data.get('leixing_id') + if not mingzi: + return Response({'code': 400, 'msg': '专区名称不能为空'}) + if not leixing_id: + return Response({'code': 400, 'msg': '请选择所属类型'}) + + try: + leixing_id = int(leixing_id) + if not ShangpinLeixing.objects.filter(id=leixing_id).exists(): + return Response({'code': 404, 'msg': '所属类型不存在'}) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '所属类型ID无效'}) + + try: + shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) + if shenhezhuangtai not in [1, 2]: + raise ValueError + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) + + try: + with transaction.atomic(): + zhuanqu = ShangpinZhuanqu.objects.create( + mingzi=mingzi, + leixing_id=leixing_id, + shenhezhuangtai=shenhezhuangtai, + paixu=0 + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': zhuanqu.id}}) + except Exception as e: + logger.error(f"添加专区失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '数据库错误'}) + + def update_zhuanqu(self, request): + zone_id = request.data.get('id') + if not zone_id: + return Response({'code': 400, 'msg': '缺少专区ID'}) + try: + zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 404, 'msg': '专区不存在'}) + + changes = {} + mingzi = request.data.get('mingzi') + if mingzi is not None and mingzi.strip() != zhuanqu.mingzi: + changes['mingzi'] = mingzi.strip() + + leixing_id = request.data.get('leixing_id') + if leixing_id is not None: + try: + new_id = int(leixing_id) + if not ShangpinLeixing.objects.filter(id=new_id).exists(): + return Response({'code': 404, 'msg': '新所属类型不存在'}) + if new_id != zhuanqu.leixing_id: + changes['leixing_id'] = new_id + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '所属类型ID无效'}) + + shenhezhuangtai = request.data.get('shenhezhuangtai') + if shenhezhuangtai is not None: + try: + new_val = int(shenhezhuangtai) + if new_val not in [1, 2]: + raise ValueError + if new_val != zhuanqu.shenhezhuangtai: + changes['shenhezhuangtai'] = new_val + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) + + if not changes: + return Response({'code': 400, 'msg': '未做任何修改'}) + + try: + with transaction.atomic(): + for field, value in changes.items(): + setattr(zhuanqu, field, value) + zhuanqu.save() + return Response({'code': 0, 'msg': '修改成功'}) + except Exception as e: + logger.error(f"更新专区失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '修改失败'}) + + def delete_zhuanqu(self, request): + zone_id = request.data.get('id') + if not zone_id: + return Response({'code': 400, 'msg': '缺少专区ID'}) + delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on'] + + try: + zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 404, 'msg': '专区不存在'}) + + with transaction.atomic(): + if delete_products: + Shangpin.objects.filter(zhuanqu_id=zone_id).delete() + else: + Shangpin.objects.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None) + zhuanqu.delete() + + return Response({'code': 0, 'msg': '删除成功'}) + + +# 继续在 houtai/views_product.py 中添加 +class GetRateView(APIView): + """ + 获取打手分成费率(平台订单和商家订单) + 权限:需要拥有 7007b + 请求:POST /houtai/hthqfhpz + 参数:{"username": "客服手机号"} + 返回:{"code":0, "data": {"platform_rate": xx, "merchant_rate": xx}} + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '7007b' not in permissions: + return Response({'code': 403, 'msg': '您无权查看费率'}) + + from dingdan.models import Lilubiao + try: + platform_rate_obj = Lilubiao.objects.get(fadanpingtai='1') + merchant_rate_obj = Lilubiao.objects.get(fadanpingtai='3') + except Lilubiao.DoesNotExist as e: + logger.error(f"费率记录缺失: {e}") + return Response({'code': 500, 'msg': '费率配置缺失'}) + + return Response({ + 'code': 0, + 'data': { + 'platform_rate': float(platform_rate_obj.lilu) if platform_rate_obj.lilu else 0, + 'merchant_rate': float(merchant_rate_obj.lilu) if merchant_rate_obj.lilu else 0, + } + }) + + +class ModifyRateView(APIView): + """ + 修改打手分成费率 + 权限:需要拥有 7007b + 请求:POST /houtai/htxgfl + 参数:{"username": "客服手机号", "type": "platform" / "merchant", "value": 费率(百分比数字)} + 返回:{"code":0, "msg":"修改成功"} + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + rate_type = request.data.get('type') + value = request.data.get('value') + + if not username or not rate_type or value is None: + return Response({'code': 400, 'msg': '缺少参数'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '7007b' not in permissions: + return Response({'code': 403, 'msg': '您无权修改费率'}) + + try: + rate_value = Decimal(str(value)) + except: + return Response({'code': 400, 'msg': '费率值格式错误'}) + + + if rate_type == 'platform': + fadanpingtai = '1' + elif rate_type == 'merchant': + fadanpingtai = '3' + else: + return Response({'code': 400, 'msg': '无效的type参数'}) + + try: + with transaction.atomic(): + rate_obj = Lilubiao.objects.select_for_update().get(fadanpingtai=fadanpingtai) + rate_obj.lilu = rate_value + rate_obj.save() + except Lilubiao.DoesNotExist: + return Response({'code': 404, 'msg': '费率记录不存在'}) + except Exception as e: + logger.error(f"修改费率失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '修改失败'}) + + return Response({'code': 0, 'msg': '修改成功'}) + + + + + + +# 权限码(根据你要求,使用 '8080a') +REQUIRED_PERMISSION = '8080a' +class PopupNoticeListAPIView(APIView): + """ + 获取所有弹窗配置(页面 + 弹窗 + 图片) + URL: POST /houtai/hthqtcxx + 请求参数: { "username": "客服账号" } + 响应格式: { "code": 0, "data": { "pages": [...] } } + 注意:所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段区分 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取前端传递的 username(用于防越权) + username_frontend = request.data.get('username') + # 2. 调用公共权限验证方法,返回 (客服对象, 权限列表) + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + # 身份验证失败,返回业务码 403,但 HTTP 状态码仍是 200 + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + + # 3. 检查是否拥有所需权限码 '8080a' + if REQUIRED_PERMISSION not in permissions: + return Response({'code': 403, 'msg': '您没有权限访问弹窗管理功能'}) + + # 4. 查询所有页面,并预加载关联的弹窗和图片(避免N+1查询) + pages = PopupPage.objects.all().prefetch_related('popups__images').order_by('id') + + # 5. 序列化输出 + serializer = PopupPageSerializer(pages, many=True) + return Response({'code': 0, 'data': {'pages': serializer.data}}) + + +class PopupNoticeModifyAPIView(APIView): + """ + 统一修改接口:支持页面、弹窗、图片的增删改 + URL: POST /houtai/htxgtcxx + 请求中必须包含 action 字段,用于区分操作类型。 + 所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段体现。 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ---------- 1. 权限验证 ---------- + username_frontend = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + if REQUIRED_PERMISSION not in permissions: + return Response({'code': 403, 'msg': '您没有权限操作弹窗管理'}) + + # ---------- 2. 获取 action 并分发 ---------- + action = request.data.get('action') + if not action: + return Response({'code': 400, 'msg': '缺少action参数'}) + + try: + if action == 'add_page': + return self.add_page(request) + elif action == 'update_page': + return self.update_page(request) + elif action == 'delete_page': + return self.delete_page(request) + elif action == 'add_popup': + return self.add_popup(request) + elif action == 'update_popup': + return self.update_popup(request) + elif action == 'delete_popup': + return self.delete_popup(request) + elif action == 'add_image': + return self.add_image(request) + elif action == 'update_image': + return self.update_image(request) + elif action == 'delete_image': + return self.delete_image(request) + else: + return Response({'code': 400, 'msg': f'未知的action: {action}'}) + except Exception as e: + # 捕获未预期的异常,返回服务器错误 + import traceback + traceback.print_exc() + return Response({'code': 500, 'msg': f'服务器错误: {str(e)}'}) + + # ==================== 页面相关操作 ==================== + + def add_page(self, request): + """添加新页面""" + data = request.data + # 必填字段校验 + if not data.get('page_key') or not data.get('name'): + return Response({'code': 400, 'msg': '缺少 page_key 或 name'}) + # 唯一性校验 + if PopupPage.objects.filter(page_key=data['page_key']).exists(): + return Response({'code': 400, 'msg': '页面标识已存在'}) + + page = PopupPage.objects.create( + page_key=data['page_key'], + name=data['name'], + description=data.get('description', ''), + is_active=data.get('is_active', True), + ignore_user_mute=data.get('ignore_user_mute', False) + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': page.id}}) + + def update_page(self, request): + """更新页面信息""" + page_id = request.data.get('id') + if not page_id: + return Response({'code': 400, 'msg': '缺少页面id'}) + try: + page = PopupPage.objects.get(id=page_id) + except PopupPage.DoesNotExist: + return Response({'code': 404, 'msg': '页面不存在'}) + + # 更新允许修改的字段 + page.page_key = request.data.get('page_key', page.page_key) + page.name = request.data.get('name', page.name) + page.description = request.data.get('description', page.description) + page.is_active = request.data.get('is_active', page.is_active) + page.ignore_user_mute = request.data.get('ignore_user_mute', page.ignore_user_mute) + page.save() + return Response({'code': 0, 'msg': '更新成功'}) + + def delete_page(self, request): + """删除页面,可级联删除其下所有弹窗和图片""" + page_id = request.data.get('id') + cascade = request.data.get('cascade_delete_popups', True) # 默认级联 + if not page_id: + return Response({'code': 400, 'msg': '缺少页面id'}) + try: + page = PopupPage.objects.get(id=page_id) + except PopupPage.DoesNotExist: + return Response({'code': 404, 'msg': '页面不存在'}) + + if cascade: + # 级联删除:使用事务保证原子性 + with transaction.atomic(): + popups = page.popups.all() + for popup in popups: + # 删除弹窗下的所有图片文件 + for img in popup.images.all(): + if img.image_url: + delete_from_oss(img.image_url) + popups.delete() + page.delete() + else: + # 非级联删除:如果页面下还有弹窗,则拒绝删除 + if page.popups.exists(): + return Response({'code': 400, 'msg': '该页面下存在弹窗,请先删除弹窗或使用级联删除'}) + page.delete() + + return Response({'code': 0, 'msg': '删除成功'}) + + # ==================== 弹窗相关操作 ==================== + + def add_popup(self, request): + """为指定页面添加新弹窗""" + data = request.data + page_id = data.get('page_id') + popup_id = data.get('popup_id') + if not page_id or not popup_id: + return Response({'code': 400, 'msg': '缺少 page_id 或 popup_id'}) + try: + page = PopupPage.objects.get(id=page_id) + except PopupPage.DoesNotExist: + return Response({'code': 404, 'msg': '页面不存在'}) + + # 同一页面下 popup_id 必须唯一 + if PopupConfig.objects.filter(page=page, popup_id=popup_id).exists(): + return Response({'code': 400, 'msg': '该页面下已存在相同的弹窗ID'}) + + popup = PopupConfig.objects.create( + page=page, + popup_id=popup_id, + title=data.get('title', ''), + description=data.get('description', ''), + strategy_type=data.get('strategy_type', 'daily'), + max_count=data.get('max_count', 1), + reset_interval=data.get('reset_interval', 'day'), + duration=data.get('duration', 0), + force_even_muted=data.get('force_even_muted', False), + sort_order=data.get('sort_order', 0), + is_active=data.get('is_active', True), + start_time=data.get('start_time') or None, + end_time=data.get('end_time') or None + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': popup.id}}) + + def update_popup(self, request): + """更新弹窗信息""" + popup_id = request.data.get('id') + if not popup_id: + return Response({'code': 400, 'msg': '缺少弹窗id'}) + try: + popup = PopupConfig.objects.get(id=popup_id) + except PopupConfig.DoesNotExist: + return Response({'code': 404, 'msg': '弹窗不存在'}) + + # 更新弹窗字段 + popup.popup_id = request.data.get('popup_id', popup.popup_id) + popup.title = request.data.get('title', popup.title) + popup.description = request.data.get('description', popup.description) + popup.strategy_type = request.data.get('strategy_type', popup.strategy_type) + popup.max_count = request.data.get('max_count', popup.max_count) + popup.reset_interval = request.data.get('reset_interval', popup.reset_interval) + popup.duration = request.data.get('duration', popup.duration) + popup.force_even_muted = request.data.get('force_even_muted', popup.force_even_muted) + popup.sort_order = request.data.get('sort_order', popup.sort_order) + popup.is_active = request.data.get('is_active', popup.is_active) + popup.start_time = request.data.get('start_time') or None + popup.end_time = request.data.get('end_time') or None + popup.save() + return Response({'code': 0, 'msg': '更新成功'}) + + def delete_popup(self, request): + """删除弹窗,同时删除其下所有图片(包括OSS文件)""" + popup_id = request.data.get('id') + if not popup_id: + return Response({'code': 400, 'msg': '缺少弹窗id'}) + try: + popup = PopupConfig.objects.get(id=popup_id) + except PopupConfig.DoesNotExist: + return Response({'code': 404, 'msg': '弹窗不存在'}) + + with transaction.atomic(): + for img in popup.images.all(): + if img.image_url: + delete_from_oss(img.image_url) + popup.images.all().delete() + popup.delete() + return Response({'code': 0, 'msg': '删除成功'}) + + # ==================== 图片相关操作 ==================== + + def _upload_image_file(self, file_obj, page_key): + """ + 内部方法:将图片上传到OSS指定目录 + 目录: a_long/tanchuang/tanchuang/ + 文件名: {page_key}_{时间戳}_{uuid8}.{ext} + 返回: 相对路径(例如 a_long/tanchuang/tanchuang/dashouzhongxin_20250419120000_abc12345.jpg) + """ + # 验证图片格式和大小 + valid, msg = validate_image(file_obj) + if not valid: + raise ValueError(msg) + + # 生成唯一文件名 + ext = os.path.splitext(file_obj.name)[1].lower() + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + unique_id = str(uuid.uuid4())[:8] + filename = f"{page_key}_{timestamp}_{unique_id}{ext}" + relative_path = f"a_long/tanchuang/tanchuang/{filename}" + + # 上传到OSS + url = upload_to_oss(file_obj, relative_path) + if not url: + raise ValueError("图片上传失败") + return relative_path + + def add_image(self, request): + """为指定弹窗添加图片(支持上传文件)""" + popup_id = request.data.get('popup_id') + if not popup_id: + return Response({'code': 400, 'msg': '缺少 popup_id'}) + try: + popup = PopupConfig.objects.get(id=popup_id) + except PopupConfig.DoesNotExist: + return Response({'code': 404, 'msg': '弹窗不存在'}) + + # 获取上传的文件 + file_obj = request.FILES.get('file') + if not file_obj: + return Response({'code': 400, 'msg': '未提供图片文件'}) + + try: + page_key = popup.page.page_key + relative_path = self._upload_image_file(file_obj, page_key) + except ValueError as e: + return Response({'code': 400, 'msg': str(e)}) + + # 保存图片记录 + image = PopupImage.objects.create( + popup_config=popup, + image_url=relative_path, + text=request.data.get('text', ''), + sort_order=request.data.get('sort_order', 0) + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': image.id}}) + + def update_image(self, request): + """更新图片信息(可替换图片文件、修改文字、排序)""" + image_id = request.data.get('id') + if not image_id: + return Response({'code': 400, 'msg': '缺少图片id'}) + try: + image = PopupImage.objects.get(id=image_id) + except PopupImage.DoesNotExist: + return Response({'code': 404, 'msg': '图片不存在'}) + + # 更新文字和排序 + image.text = request.data.get('text', image.text) + image.sort_order = request.data.get('sort_order', image.sort_order) + + # 如果提供了新图片文件,则替换 + file_obj = request.FILES.get('file') + if file_obj: + try: + page_key = image.popup_config.page.page_key + new_path = self._upload_image_file(file_obj, page_key) + except ValueError as e: + return Response({'code': 400, 'msg': str(e)}) + # 删除旧图片 + if image.image_url: + delete_from_oss(image.image_url) + image.image_url = new_path + + image.save() + return Response({'code': 0, 'msg': '更新成功'}) + + def delete_image(self, request): + """删除图片(同时删除OSS文件)""" + image_id = request.data.get('id') + if not image_id: + return Response({'code': 400, 'msg': '缺少图片id'}) + try: + image = PopupImage.objects.get(id=image_id) + except PopupImage.DoesNotExist: + return Response({'code': 404, 'msg': '图片不存在'}) + + if image.image_url: + delete_from_oss(image.image_url) + image.delete() + return Response({'code': 0, 'msg': '删除成功'}) + + + +from shangdian.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng +class ShopListView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 调用公共权限验证(与您给的示例完全一致) + username_frontend = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + + # 2. 检查是否拥有所需权限(999a~999e 任意一个) + required_perms = {'999a', '999b', '999c', '999d', '999e'} + if not required_perms.intersection(set(permissions)): + return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'}) + + # 3. 解析请求参数 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip() + zhuangtai = request.data.get('zhuangtai') + lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip() + user_id = request.data.get('user_id', '').strip() + ketixian_yue_min = request.data.get('ketixian_yue_min') + ketixian_yue_max = request.data.get('ketixian_yue_max') + zhengqu_zonge_min = request.data.get('zhengqu_zonge_min') + zhengqu_zonge_max = request.data.get('zhengqu_zonge_max') + + # 4. 构建查询集(先查店铺,预加载关联凭证) + queryset = Dianpu.objects.select_related('pingzheng').all() + + if dianpu_mingcheng: + queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng) + if zhuangtai is not None: + queryset = queryset.filter(zhuangtai=zhuangtai) + if lianxi_dianhua: + queryset = queryset.filter(lianxi_dianhua__icontains=lianxi_dianhua) + if user_id: + queryset = queryset.filter(pingzheng__yonghu=user_id) + + # 金额范围过滤 + if ketixian_yue_min is not None: + queryset = queryset.filter(ketixian_yue__gte=ketixian_yue_min) + if ketixian_yue_max is not None: + queryset = queryset.filter(ketixian_yue__lte=ketixian_yue_max) + if zhengqu_zonge_min is not None: + queryset = queryset.filter(zhengqu_zonge__gte=zhengqu_zonge_min) + if zhengqu_zonge_max is not None: + queryset = queryset.filter(zhengqu_zonge__lte=zhengqu_zonge_max) + + # 5. 分页 + paginator = Paginator(queryset, page_size) + page_obj = paginator.get_page(page) + + # 6. 构造返回数据 + shop_list = [] + for dianpu in page_obj: + pingzheng = dianpu.pingzheng + shop_list.append({ + 'id': dianpu.id, + 'dianpu_mingcheng': dianpu.dianpu_mingcheng, + 'dianpu_touxiang': dianpu.dianpu_touxiang or '', + 'lianxi_dianhua': dianpu.lianxi_dianhua or '', + 'weixinhao': dianpu.weixinhao or '', + 'zhuangtai': dianpu.zhuangtai, + 'bangding_yonghushu': dianpu.bangding_yonghushu, + 'erweima_url': dianpu.erweima_url or '', + 'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng, + 'shouyi_choucheng_feilv': dianpu.shouyi_choucheng_feilv or 0, + 'ketixian_yue': dianpu.ketixian_yue, + 'zhengqu_zonge': dianpu.zhengqu_zonge, + 'meiri_tixian_xiane': dianpu.meiri_tixian_xiane or 0, + 'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane, + 'pingzheng__yonghu': pingzheng.yonghu, # 用户ID + 'pingzheng__zhanghao': pingzheng.zhanghao, # 登录账号 + }) + + # 获取默认配置(ID=1,不存在则创建) + default_config, _ = DianpuMorenPeizhi.objects.get_or_create(id=1, defaults={ + 'moren_choucheng_feilv': 0.1, + 'moren_tixian_xiane': 1000.00 + }) + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'list': shop_list, + 'total': paginator.count, + 'default_config': { + 'moren_choucheng_feilv': default_config.moren_choucheng_feilv, + 'moren_tixian_xiane': default_config.moren_tixian_xiane, + } + } + }) + +from django.db import transaction, IntegrityError +class ShopModifyView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 身份验证 + username_frontend = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + + # 转换权限为集合方便判断 + user_perms = set(permissions) + action = request.data.get('action', '') + + # 2. 根据 action 分派任务 + if action == 'update_dianpu': + return self._update_dianpu(request, user_perms) + elif action == 'add_dianpu': + return self._add_dianpu(request, user_perms) + elif action == 'update_default': + return self._update_default(request, user_perms) + else: + return Response({'code': 400, 'msg': '未知操作类型'}) + + def _update_dianpu(self, request, user_perms): + """修改店铺信息,精确权限控制""" + dianpu_id = request.data.get('dianpu_id') + if not dianpu_id: + return Response({'code': 400, 'msg': '缺少店铺ID'}) + + try: + with transaction.atomic(): + dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id) + + # 基础信息修改 (999e) + if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']): + if '999e' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改店铺基本信息(999e)'}) + if 'dianpu_mingcheng' in request.data: + dianpu.dianpu_mingcheng = request.data['dianpu_mingcheng'] + if 'lianxi_dianhua' in request.data: + dianpu.lianxi_dianhua = request.data['lianxi_dianhua'] + if 'weixinhao' in request.data: + dianpu.weixinhao = request.data['weixinhao'] + + # 店铺状态 (999a) + if 'zhuangtai' in request.data: + if '999a' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改店铺状态(999a)'}) + new_status = int(request.data['zhuangtai']) + if new_status not in (0, 1): + return Response({'code': 400, 'msg': '店铺状态无效'}) + dianpu.zhuangtai = new_status + + # 可提现余额 (999b) + if 'ketixian_yue' in request.data: + if '999b' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改可提现余额(999b)'}) + dianpu.ketixian_yue = request.data['ketixian_yue'] + + # 提现限额相关 (999c) + if 'kaiqi_meiri_xiane' in request.data or 'meiri_tixian_xiane' in request.data: + if '999c' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改提现限额配置(999c)'}) + if 'kaiqi_meiri_xiane' in request.data: + dianpu.kaiqi_meiri_xiane = request.data['kaiqi_meiri_xiane'] + if 'meiri_tixian_xiane' in request.data: + dianpu.meiri_tixian_xiane = request.data['meiri_tixian_xiane'] + + # 抽成相关 (999d) + if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data: + if '999d' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改抽成配置(999d)'}) + if 'kaiqi_shouyi_choucheng' in request.data: + dianpu.kaiqi_shouyi_choucheng = request.data['kaiqi_shouyi_choucheng'] + if 'shouyi_choucheng_feilv' in request.data: + dianpu.shouyi_choucheng_feilv = request.data['shouyi_choucheng_feilv'] + + dianpu.save() # Django 会自动判断更新哪些字段 + return Response({'code': 0, 'msg': '店铺信息更新成功'}) + + except Dianpu.DoesNotExist: + return Response({'code': 404, 'msg': '店铺不存在'}) + except Exception as e: + logger.exception("修改店铺失败") + return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) + + def _add_dianpu(self, request, user_perms): + """添加店铺,需要权限 999b""" + if '999b' not in user_perms: + return Response({'code': 403, 'msg': '无权限添加店铺(999b)'}) + + yonghu_id = request.data.get('yonghu_id') + zhanghao = request.data.get('zhanghao') + mima = request.data.get('mima') + dianpu_mingcheng = request.data.get('dianpu_mingcheng') + + if not all([yonghu_id, zhanghao, mima, dianpu_mingcheng]): + return Response({'code': 400, 'msg': '必填字段缺失(用户ID、账号、密码、店铺名称)'}) + + # 检查用户是否存在 + if not UserMain.objects.filter(yonghuid=yonghu_id).exists(): + return Response({'code': 404, 'msg': '用户不存在'}) + + # 检查凭证是否已存在 + if YonghuPingzheng.objects.filter(zhanghao=zhanghao).exists(): + return Response({'code': 400, 'msg': '登录账号已存在'}) + if YonghuPingzheng.objects.filter(yonghu=yonghu_id).exists(): + return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'}) + + try: + with transaction.atomic(): + pingzheng = YonghuPingzheng.objects.create( + yonghu=yonghu_id, + zhanghao=zhanghao, + mima=mima, + is_active=True + ) + dianpu = Dianpu.objects.create( + pingzheng=pingzheng, + dianpu_mingcheng=dianpu_mingcheng, + lianxi_dianhua=request.data.get('lianxi_dianhua', ''), + weixinhao=request.data.get('weixinhao', ''), + kaiqi_shouyi_choucheng=request.data.get('kaiqi_shouyi_choucheng', False), + shouyi_choucheng_feilv=request.data.get('shouyi_choucheng_feilv', 0), + kaiqi_meiri_xiane=request.data.get('kaiqi_meiri_xiane', False), + meiri_tixian_xiane=request.data.get('meiri_tixian_xiane', 0), + ) + return Response({'code': 0, 'msg': '店铺创建成功', 'dianpu_id': dianpu.id}) + except IntegrityError as e: + logger.error("创建店铺违反唯一性约束: %s", e) + return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'}) + except Exception as e: + logger.exception("创建店铺失败") + return Response({'code': 500, 'msg': '创建失败,请稍后重试'}) + + def _update_default(self, request, user_perms): + """修改默认配置,按修改内容校验权限""" + feilv = request.data.get('moren_choucheng_feilv') + xiane = request.data.get('moren_tixian_xiane') + modify_commission = feilv is not None + modify_withdraw = xiane is not None + + if modify_commission and '999d' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'}) + if modify_withdraw and '999c' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'}) + + try: + config, _ = DianpuMorenPeizhi.objects.get_or_create(id=1) + if modify_commission: + config.moren_choucheng_feilv = feilv + if modify_withdraw: + config.moren_tixian_xiane = xiane + config.save(update_fields=(['moren_choucheng_feilv'] if modify_commission else []) + + (['moren_tixian_xiane'] if modify_withdraw else [])) + return Response({'code': 0, 'msg': '默认配置更新成功'}) + except Exception as e: + logger.exception("更新默认配置失败") + return Response({'code': 500, 'msg': '更新失败'}) + + + + + + + +# houtai/views.py (完整新增接口,可直接追加到文件末尾) +import logging +from django.db import models # Q 查询必须导入 +from django.core.paginator import Paginator +from django.db import transaction +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from shangpin.models import ShangpinLeixing # 公共商品类型 +from shangdian.models import ( + Dianpu, ShangpinLeixingDianpu, + DianpuShangpinShenheShezhi, YonghuPingzheng +) +from .utils import verify_kefu_permission # 已提供的公共身份校验方法 + +logger = logging.getLogger(__name__) + +# 店铺商品管理所需权限组(拥有任意一个即可访问列表类接口) +SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd'] + + +# ==================== 1. 公共商品类型 + 审核模式 ==================== +class ShopPublicTypeAndAuditView(APIView): + """ + 返回:①正常状态的公共商品类型列表 ②全局商品审核模式开关 + 前端请求路径:/houtai/htdphqsplx + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ---------- 身份与权限校验 ---------- + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + + # 拥有 1199ab/1199abc/1199abd 任一个即可访问 + if not set(permissions).intersection(SHOP_PRODUCT_PERMS): + return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'}) + + # ---------- 查询公共商品类型(仅正常审核状态的) ---------- + try: + # shenhezhuangtai = 1 表示正常 (根据模型默认值) + types_qs = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values( + 'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', 'is_cross_enabled' + ) + type_list = [] + for t in types_qs: + type_list.append({ + 'id': t['id'], + 'jieshao': t['jieshao'] or '', + 'tupian_url': t['tupian_url'] or '', + 'yaoqiuleixing': t['yaoqiuleixing'], + 'huiyuan_id': t['huiyuan_id'], + 'yongjin': t['yongjin'], + 'is_cross_enabled': t['is_cross_enabled'], + }) + except Exception as e: + logger.exception("查询公共商品类型失败") + return Response({'code': 500, 'msg': '服务器错误,请稍后重试'}) + + # ---------- 获取审核模式配置 ---------- + audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first() + kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False + + return Response({ + 'code': 0, + 'data': { + 'public_types': type_list, + 'kaiqi_shenhe': kaiqi_shenhe + } + }) + + +# ==================== 2. 店铺列表(支持搜索、筛选、分页) ==================== +class ShopListForProductView(APIView): + """ + 返回店铺列表,用于商品管理页面选择店铺。 + 支持:关键词搜索(店铺ID/名称/用户ID)、店铺状态筛选、分页。 + 前端请求路径:/houtai/htdphqdpys + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + if not set(permissions).intersection(SHOP_PRODUCT_PERMS): + return Response({'code': 403, 'msg': '无权访问店铺列表'}) + + # ---------- 安全解析分页参数 ---------- + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '分页参数格式错误'}) + + # 搜索关键词(字符串,ORM 自动防注入) + keyword = str(request.data.get('keyword', '')).strip() + # 店铺状态筛选(1正常,0封禁,不传则全部) + zhuangtai = request.data.get('zhuangtai') + + # ---------- 构建查询集(关联凭证表获取用户ID) ---------- + shops = Dianpu.objects.select_related('pingzheng').all() + + # 状态筛选:只接受 0 或 1 + if zhuangtai is not None: + try: + zhuangtai = int(zhuangtai) + if zhuangtai in [0, 1]: + shops = shops.filter(zhuangtai=zhuangtai) + else: + return Response({'code': 400, 'msg': '状态参数只能为0或1'}) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '状态参数必须为整数'}) + + # 关键词搜索(店铺ID/名称/用户ID) + if keyword: + shops = shops.filter( + models.Q(id__icontains=keyword) | + models.Q(dianpu_mingcheng__icontains=keyword) | + models.Q(pingzheng__yonghu__icontains=keyword) + ) + + # ---------- 分页 ---------- + paginator = Paginator(shops, page_size) + page_obj = paginator.get_page(page) + + shop_list = [] + for shop in page_obj: + shop_list.append({ + 'id': shop.id, + 'dianpu_mingcheng': shop.dianpu_mingcheng, + 'zhuangtai': shop.zhuangtai, + 'yonghu_id': shop.pingzheng.yonghu if shop.pingzheng else '', + 'lianxi_dianhua': shop.lianxi_dianhua or '', + 'weixinhao': shop.weixinhao or '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': shop_list, + 'total': paginator.count + } + }) + + +# ==================== 3. 店铺商品类型映射列表(含筛选、待审核计数) ==================== +class ShopProductTypeMappingView(APIView): + """ + 返回店铺商品类型映射(ShangpinLeixingDianpu)列表。 + 支持:店铺ID、公共类型ID、上架/封禁/审核状态筛选,关键字搜索(介绍/ID/店铺名/公共类型名)。 + 每次请求同时返回“待审核总数”(shenhe_zhuangtai=False 的记录数)。 + 前端请求路径:/houtai/htdphqyssplx + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + if not set(permissions).intersection(SHOP_PRODUCT_PERMS): + return Response({'code': 403, 'msg': '无权访问商品类型映射'}) + + # ---------- 安全解析参数 ---------- + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '分页参数格式错误'}) + + keyword = str(request.data.get('keyword', '')).strip() + shop_id = request.data.get('shop_id') + public_type_id = request.data.get('public_type_id') + + # 布尔型筛选字段处理:None 表示不筛选,否则转为布尔值 + def parse_bool(val): + if val is None: + return None + return bool(val) + + shangjia = parse_bool(request.data.get('shangjia_zhuangtai')) + fengjin = parse_bool(request.data.get('fengjin_zhuangtai')) + shenhe = parse_bool(request.data.get('shenhe_zhuangtai')) + + # ---------- 查询(连表店铺和公共类型) ---------- + mappings = ShangpinLeixingDianpu.objects.select_related('dianpu', 'gonggong_leixing').all() + + # 筛选条件:均为参数化,完全防注入 + if shop_id is not None: + mappings = mappings.filter(dianpu_id=int(shop_id)) + if public_type_id is not None: + mappings = mappings.filter(gonggong_leixing_id=int(public_type_id)) + if shangjia is not None: + mappings = mappings.filter(shangjia_zhuangtai=shangjia) + if fengjin is not None: + mappings = mappings.filter(fengjin_zhuangtai=fengjin) + if shenhe is not None: + mappings = mappings.filter(shenhe_zhuangtai=shenhe) + + # 关键字搜索(介绍、ID、店铺名、公共类型名) + if keyword: + mappings = mappings.filter( + models.Q(jieshao__icontains=keyword) | + models.Q(id__icontains=keyword) | + models.Q(dianpu__dianpu_mingcheng__icontains=keyword) | + models.Q(gonggong_leixing__jieshao__icontains=keyword) + ) + + # ---------- 待审核总数(不受分页影响) ---------- + pending_total = mappings.filter(shenhe_zhuangtai=False).count() + + # ---------- 分页 ---------- + paginator = Paginator(mappings, page_size) + page_obj = paginator.get_page(page) + + mapping_list = [] + for m in page_obj: + mapping_list.append({ + 'id': m.id, + 'dianpu_id': m.dianpu_id, + 'dianpu_name': m.dianpu.dianpu_mingcheng if m.dianpu else '', + 'gonggong_leixing_id': m.gonggong_leixing_id, + 'gonggong_leixing_name': m.gonggong_leixing.jieshao if m.gonggong_leixing else '', + 'jieshao': m.jieshao, + 'tupian_url': m.tupian_url or '', + 'paixu': m.paixu, + 'shangjia_zhuangtai': m.shangjia_zhuangtai, + 'fengjin_zhuangtai': m.fengjin_zhuangtai, + 'shenhe_zhuangtai': m.shenhe_zhuangtai, + }) + + return Response({ + 'code': 0, + 'data': { + 'list': mapping_list, + 'total': paginator.count, + 'pending_total': pending_total + } + }) + + +# ==================== 统一修改接口(已修正事务) ==================== +class ShopProductModifyView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + + user_perms = set(permissions) + action = request.data.get('action') + + if action == 'update_audit_mode': + return self._update_audit_mode(request, user_perms) + elif action == 'update_mapping_status': + return self._update_mapping_status(request, user_perms) + else: + return Response({'code': 400, 'msg': '未知操作'}) + + @transaction.atomic # ✅ 事务修复点 + def _update_audit_mode(self, request, user_perms): + if '1199ab' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改审核模式'}) + + kaiqi = request.data.get('kaiqi_shenhe') + if kaiqi is None: + return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'}) + + try: + config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1) + config.kaiqi_shenhe = bool(kaiqi) + config.save(update_fields=['kaiqi_shenhe']) + return Response({'code': 0, 'msg': '审核模式已更新'}) + except Exception as e: + logger.exception("更新审核模式失败") + return Response({'code': 500, 'msg': '服务器错误'}) + + @transaction.atomic + def _update_mapping_status(self, request, user_perms): + mapping_id = request.data.get('mapping_id') + if not mapping_id: + return Response({'code': 400, 'msg': '缺少 mapping_id'}) + + try: + mapping = ShangpinLeixingDianpu.objects.select_for_update().get(id=int(mapping_id)) + except (ShangpinLeixingDianpu.DoesNotExist, ValueError): + return Response({'code': 404, 'msg': '映射不存在'}) + + fengjin = request.data.get('fengjin_zhuangtai') + shangjia = request.data.get('shangjia_zhuangtai') + shenhe = request.data.get('shenhe_zhuangtai') + + # 权限判断 + if fengjin is not None: + if fengjin: + if '1199abc' not in user_perms: + return Response({'code': 403, 'msg': '无权限封禁'}) + else: + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限解封'}) + mapping.fengjin_zhuangtai = bool(fengjin) + + if shangjia is not None: + if not shangjia: + if '1199abc' not in user_perms: + return Response({'code': 403, 'msg': '无权限下架'}) + else: + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限上架'}) + mapping.shangjia_zhuangtai = bool(shangjia) + + if shenhe is not None: + if shenhe: + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限审核通过'}) + mapping.shenhe_zhuangtai = bool(shenhe) + + mapping.save() + return Response({'code': 0, 'msg': '修改成功'}) + + + + +# 商品管理相关权限组(拥有任意一个即可查看列表) +PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd', '1199abe'] + + +class ProductListView(APIView): + """ + 商品列表接口,支持多条件筛选与分页。 + 请求路径: /houtai/hqhtdpsplx + 权限要求: 1199ab / 1199abc / 1199abd / 1199abe 任一即可 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ----- 身份与权限验证 ----- + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + + if not set(permissions).intersection(PRODUCT_PERMS): + return Response({'code': 403, 'msg': '无商品管理权限'}) + + # ----- 安全解析分页与筛选参数 ----- + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 15)) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '分页参数格式错误'}) + + keyword = str(request.data.get('keyword', '')).strip() + shop_id = request.data.get('shop_id') # 店铺ID (可选) + shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型映射ID (可选) + public_type_id = request.data.get('public_type_id') # 公共商品类型ID (可选) + + # 布尔型状态筛选 + def parse_bool(val): + if val is None: + return None + return bool(val) + + shenhe = parse_bool(request.data.get('shenhe_zhuangtai')) + shangjia = parse_bool(request.data.get('shangjia_zhuangtai')) + fengjin = parse_bool(request.data.get('fengjin_zhuangtai')) + + # ----- 构建查询(预加载店铺和店铺商品类型) ----- + products = Shangpin.objects.select_related('dianpu', 'dianpu_leixing').all() + + # 按店铺筛选 + if shop_id is not None: + try: + products = products.filter(dianpu_id=int(shop_id)) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': 'shop_id 格式错误'}) + + # 按店铺商品类型映射筛选 + if shop_product_type_id is not None: + try: + products = products.filter(dianpu_leixing_id=int(shop_product_type_id)) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'}) + + # 按公共商品类型筛选(leixing_id 存储的是公共类型的ID) + if public_type_id is not None: + try: + products = products.filter(leixing_id=int(public_type_id)) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': 'public_type_id 格式错误'}) + + # 平台审核状态筛选 (BooleanField) + if shenhe is not None: + products = products.filter(shenhe_zhuangtai=shenhe) + + # 上架状态筛选 + if shangjia is not None: + products = products.filter(shangjia_zhuangtai=shangjia) + + # 封禁状态筛选 + if fengjin is not None: + products = products.filter(fengjin_zhuangtai=fengjin) + + # 关键词搜索:商品ID或商品标题(搜索具有模糊性,使用icontains) + if keyword: + # 尝试纯数字可能是商品ID,使用精确匹配与标题模糊搜索 + products = products.filter( + models.Q(id__icontains=keyword) | + models.Q(biaoqian__icontains=keyword) + ) + + # ----- 获取待审核总数(平台审核未通过的数量) ----- + pending_total = products.filter(shenhe_zhuangtai=False).count() + + # ----- 分页 ----- + paginator = Paginator(products, page_size) + page_obj = paginator.get_page(page) + + # ----- 构造返回数据(批量获取公共类型名称,避免N+1) ----- + # 提取所有公共商品类型ID + type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None} + public_type_map = {} + if type_ids: + public_types = ShangpinLeixing.objects.filter(id__in=type_ids).values('id', 'jieshao') + public_type_map = {t['id']: t['jieshao'] for t in public_types} + + product_list = [] + for p in page_obj: + product_list.append({ + 'id': p.id, + 'biaoqian': p.biaoqian or '', + 'jiage': p.jiage, + 'kucun': p.kucun, + 'leixing_id': p.leixing_id, + 'public_type_name': public_type_map.get(p.leixing_id, ''), + 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, + 'duiwai_xiaoliang': p.duiwai_xiaoliang, + 'jieshao': p.jieshao or '', + 'xiadan_xuzhi': p.xiadan_xuzhi or '', + 'guize_tupian': p.guize_tupian or '', + 'yaoqiuleixing': p.yaoqiuleixing, + 'huiyuan_id': p.huiyuan_id or '', + 'yongjin': p.yongjin, + 'kaioi_ewai_dashou_fencheng': p.kaioi_ewai_dashou_fencheng, + 'ewai_dashou_fencheng': p.ewai_dashou_fencheng, + 'create_time': p.create_time.isoformat() if p.create_time else '', + 'update_time': p.update_time.isoformat() if p.update_time else '', + 'paixu': p.paixu, + 'dianpu_id': p.dianpu_id, + 'dianpu_name': p.dianpu.dianpu_mingcheng if p.dianpu else '', + 'dianpu_leixing_id': p.dianpu_leixing_id, + 'dianpu_leixing_name': p.dianpu_leixing.jieshao if p.dianpu_leixing else '', + 'shangjia_zhuangtai': p.shangjia_zhuangtai, + 'fengjin_zhuangtai': p.fengjin_zhuangtai, + 'shenhe_zhuangtai': p.shenhe_zhuangtai, + }) + + return Response({ + 'code': 0, + 'data': { + 'list': product_list, + 'total': paginator.count, + 'pending_total': pending_total, # 待审核商品总数 + } + }) + + +class ProductModifyView(APIView): + """ + 商品修改接口,根据修改内容细粒度鉴权。 + 请求路径: /houtai/htxgdpspsj + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + + user_perms = set(permissions) + action = request.data.get('action', '') + + if action != 'update_product': + return Response({'code': 400, 'msg': '未知操作'}) + + return self._update_product(request, user_perms) + + @transaction.atomic + def _update_product(self, request, user_perms): + """执行商品修改,严格校验权限""" + product_id = request.data.get('product_id') + if not product_id: + return Response({'code': 400, 'msg': '缺少商品ID'}) + + try: + # 行级锁防止并发修改 + product = Shangpin.objects.select_for_update().get(id=int(product_id)) + except (Shangpin.DoesNotExist, ValueError): + return Response({'code': 404, 'msg': '商品不存在'}) + + # 需要修改的字段 + fields_to_update = {} + # 将要修改的状态标志用于后续权限判断 + status_changes = { + 'shangjia': request.data.get('shangjia_zhuangtai'), + 'fengjin': request.data.get('fengjin_zhuangtai'), + 'shenhe': request.data.get('shenhe_zhuangtai'), + } + + # ---------- 权限分类检查 ---------- + # 1. 封禁/下架操作 (消极操作) -> 1199abc + if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']): + if '1199abc' not in user_perms: + return Response({'code': 403, 'msg': '无权限执行下架/封禁/驳回审核操作(1199abc)'}) + if status_changes.get('fengjin') is not None and status_changes['fengjin'] == True: + if '1199abc' not in user_perms: + return Response({'code': 403, 'msg': '无权限执行封禁操作(1199abc)'}) + + # 2. 解封/上架/过审 (积极操作) -> 1199abd + if any(status_changes.get(k) is not None and status_changes[k] == True for k in ['shangjia', 'shenhe']): + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限执行上架/解封/审核通过操作(1199abd)'}) + if status_changes.get('fengjin') is not None and status_changes['fengjin'] == False: + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'}) + + # 3. 修改其他数据(非状态字段)-> 1199abe + # 除了上架、封禁、审核之外的字段都属于“其他数据” + other_fields = [k for k in request.data.keys() if k not in ['action', 'product_id', 'username', + 'shangjia_zhuangtai', 'fengjin_zhuangtai', + 'shenhe_zhuangtai']] + if other_fields and '1199abe' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'}) + + # ---------- 应用修改 ---------- + # 允许修改的字段白名单(防止注入) + allowed_fields = [ + 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'duiwai_xiaoliang', + 'jieshao', 'xiadan_xuzhi', 'yaoqiuleixing', 'yongjin', + 'kaioi_ewai_dashou_fencheng', 'ewai_dashou_fencheng', + 'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai' + ] + + for field in allowed_fields: + value = request.data.get(field) + if value is not None: + # 简单类型校验(根据字段类型) + if field in ['jiage', 'kucun', 'duiwai_xiaoliang', 'yongjin', 'ewai_dashou_fencheng', 'paixu']: + try: + value = float(value) if 'jiage' in field or 'yongjin' in field or 'ewai' in field else int(value) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': f'字段 {field} 值格式错误'}) + setattr(product, field, value) + + product.save(update_fields=allowed_fields) + return Response({'code': 0, 'msg': '商品修改成功'}) + + + +# 与前端约定的聊天权限码 +CHAT_PERM_CODES = ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4'] + +class KefuChatPermissionsView(APIView): + """ + 获取客服聊天权限及GoEasy配置 + POST /houtai/kfhqltqx + Body: { "phone": "客服手机号" } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 从前端获取传递的账号,用于防越权校验 + username_from_frontend = request.data.get('phone', None) + + # 2. 调用公共验证方法。 + # 返回 (kefu_obj, permissions) 或 (None, Response) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + + # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值(错误Response) + if kefu_obj is None: + return permissions + + # ---------- 验证通过,permissions 已经是权限码列表 ---------- + # 4. 检查是否拥有至少一个聊天权限 + has_chat_perm = any(p in CHAT_PERM_CODES for p in permissions) + + # 5. 若有聊天权限,查询我方俱乐部的 chat_api_key + goeasy_appkey = '' + if has_chat_perm: + config = ClubConfig.objects.filter(is_self=1).first() + if config: + goeasy_appkey = config.chat_api_key or '' + + # 6. 返回数据 + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'permissions': permissions, + 'goeasy_appkey': goeasy_appkey + } + }) + + + + + +from dingdan.models import Dingdan, Fadan, CrossPlatformOrderData # 按实际路径 +class FineApplyView(APIView): + """ + 客服申请罚款接口(金额罚款,非积分) + 路径:POST /houtai/kffkdssq + 参数: + username: 客服手机号 + dingdan_id: 订单ID + chufaliyou: 罚款原因 + fakuanjine: 罚款金额(元) + yingxiang_qiangdan: 1影响抢单 / 0不影响抢单 + shenqingren_shenfen: 操作人身份 (1客服/2售后/3管理员等) + 权限:JWT + 权限码99933abs + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 提取参数 + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + chufaliyou = request.data.get('chufaliyou', '').strip() + fakuanjine = request.data.get('fakuanjine', 0) + yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) + shenqingren_shenfen = request.data.get('shenqingren_shenfen', 2) + + if not all([username, dingdan_id, chufaliyou, fakuanjine]): + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + # 2. 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '99933abs' not in permissions: + return Response({'code': 403, 'msg': '您无罚款权限'}) + + # 3. 获取订单(无需 select_for_update,因不修改订单) + try: + order = Dingdan.objects.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}) + + dashou_id = order.jiedan_dashou_id + if not dashou_id: + return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'}) + + # 4. 重复罚单检查 + if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): + return Response({'code': 400, 'msg': '该打手已被罚款过'}) + + # 5. 跨平台相关字段 + partner_order_id = order.partner_order_id or '' + partner_club_id = order.partner_club_id or '' + dispatch_type = order.dispatch_type or 0 + + # 本地创建罚单的可复用函数 + def create_local_fadan(): + Fadan.objects.create( + beichufa_id=dashou_id, + shenqing_chufa=username, + shenfen=1, # 1 代表打手 + chufaliyou=chufaliyou, + fakuanjine=fakuanjine, + guanliandingdan_id=dingdan_id, + zhuangtai=1, # 待缴纳 + yingxiang_qiangdan=yingxiang_qiangdan, + shenqingren_shenfen=shenqingren_shenfen, + partner_club_id='', + ) + + # 6. 判断:没有对方订单ID,或对方派单 → 本地处罚 + if not partner_order_id or dispatch_type == 2: + with transaction.atomic(): + create_local_fadan() + logger.info(f"本地罚款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}") + return Response({'code': 0, 'msg': '罚款已生成'}) + + # 7. 我方派单且有对方订单ID → 通知对方平台 + # 获取我方俱乐部ID + try: + my_club = ClubConfig.objects.filter(is_self=1).first() + if not my_club: + return Response({'code': 500, 'msg': '系统配置错误'}) + self_club_id = my_club.club_id + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain + except ObjectDoesNotExist: + return Response({'code': 500, 'msg': '对方平台信息未配置'}) + + url = partner_domain.rstrip('/') + '/houtai/kffkdssq_notify' + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id, + 'chufaliyou': chufaliyou, + 'fakuanjine': fakuanjine, + 'yingxiang_qiangdan': yingxiang_qiangdan, + 'shenqingren_shenfen': shenqingren_shenfen, + 'operator': username, + } + timeout = 3 + retries = 2 + success = False + for attempt in range(retries): + try: + resp = requests.post(url, json=payload, timeout=timeout) + if resp.status_code == 200 and resp.json().get('code') == 0: + success = True + break + except Exception: + continue + + if not success: + return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'}) + + # 通知成功后,本地也记录一笔(标记为跨平台) + with transaction.atomic(): + Fadan.objects.create( + beichufa_id=dashou_id, + shenqing_chufa=username, + shenfen=1, + chufaliyou=chufaliyou, + fakuanjine=fakuanjine, + guanliandingdan_id=dingdan_id, + zhuangtai=1, + yingxiang_qiangdan=yingxiang_qiangdan, + shenqingren_shenfen=shenqingren_shenfen, + partner_club_id=partner_club_id, + ) + logger.info(f"跨平台罚款通知成功: {dingdan_id}") + return Response({'code': 0, 'msg': '罚款已提交至对方平台'}) + + except Exception as e: + logger.error(f"罚款申请异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + +from rest_framework.permissions import AllowAny + +class PartnerFineNotifyView(APIView): + """ + 接收对方平台罚款通知(无认证) + 路径:POST /houtai/kffkdssq_notify + 参数: + order_id: 对方平台的订单ID(需通过这个找到我方订单) + club_id: 对方俱乐部ID + chufaliyou: 罚款原因 + fakuanjine: 罚款金额 + yingxiang_qiangdan: 1影响抢单 / 0不影响 + shenqingren_shenfen: 申请处罚人身份 + operator: 操作人标识 + """ + authentication_classes = [] + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + try: + order_id = request.data.get('order_id', '').strip() # 对方平台订单ID + club_id = request.data.get('club_id', '').strip() + chufaliyou = request.data.get('chufaliyou', '').strip() + fakuanjine = request.data.get('fakuanjine', 0) + yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) + shenqingren_shenfen = request.data.get('shenqingren_shenfen', 2) + operator = request.data.get('operator', '').strip() + + if not all([order_id, chufaliyou]): + return Response({'code': 400, 'msg': '参数不完整'}) + + # 通过对方订单ID查找我方订单 + try: + cross = CrossPlatformOrderData.objects.get(partner_order_id=order_id) + my_dingdan_id = cross.dingdan_id + order = Dingdan.objects.get(dingdan_id=my_dingdan_id) + dashou_id = order.jiedan_dashou_id + except CrossPlatformOrderData.DoesNotExist: + return Response({'code': 404, 'msg': '未找到关联订单'}) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '我方订单不存在'}) + + if not dashou_id: + return Response({'code': 400, 'msg': '订单无接单打手'}) + + # 重复检查 + if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=my_dingdan_id).exists(): + return Response({'code': 0, 'msg': '已存在罚单'}) + + # 创建罚单 + Fadan.objects.create( + beichufa_id=dashou_id, + shenqing_chufa=operator, + shenfen=1, + chufaliyou=chufaliyou, + fakuanjine=fakuanjine, + guanliandingdan_id=my_dingdan_id, + zhuangtai=1, + yingxiang_qiangdan=yingxiang_qiangdan, + shenqingren_shenfen=shenqingren_shenfen, + partner_club_id=club_id, + ) + logger.info(f"接收对方罚款成功: 订单 {my_dingdan_id}, 打手 {dashou_id}") + return Response({'code': 0, 'msg': 'success'}) + + except Exception as e: + logger.error(f"接收罚款通知异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + + + + + + + + +from dingdan.models import Fadan, FadanShensuTupian + + +# 权限码到身份的映射 +PERMISSION_TO_SHENFEN = { + '66693a': 1, # 打手 + '66693b': 3, # 商家 + '66693c': 2, # 管事 + '66694c': 4, # 组长 +} + +# 身份对应的用户扩展关系名 +SHENFEN_PROFILE_MAP = { + 1: 'dashou_profile', + 2: 'guanshi_profile', + 3: 'shop_profile', + 4: 'zuzhang_profile', +} + + +def check_fadan_permission(permissions, shenfen): + """检查是否有处理指定身份罚单的权限""" + perm_code = {1: '66693a', 2: '66693c', 3: '66693b', 4: '66694c'}.get(shenfen) + return perm_code in permissions + + +class FaKuanTongJiView(APIView): + """ + 罚款统计接口 + POST /houtai/htglyhqcfsltj + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 至少有一个罚款管理权限 + if not any(p in permissions for p in PERMISSION_TO_SHENFEN.keys()): + return Response({'code': 403, 'msg': '无权限查看罚款统计'}) + + stats = Fadan.objects.aggregate( + total=Count('id'), + daijiaona=Count('id', filter=Q(zhuangtai=1)), + shensuzhong=Count('id', filter=Q(zhuangtai=3)), + yijiaona=Count('id', filter=Q(zhuangtai=2)), + yibohui=Count('id', filter=Q(zhuangtai=4)), + ) + data = {k: v or 0 for k, v in stats.items()} + return Response({'code': 0, 'msg': '成功', 'data': data}) + except Exception as e: + logger.error(f"罚款统计异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + +class FaKuanLieBiaoView(APIView): + """ + 罚款列表接口 + POST /houtai/hthqfklb + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if not any(p in permissions for p in PERMISSION_TO_SHENFEN.keys()): + return Response({'code': 403, 'msg': '无权限查看罚款列表'}) + + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + if page_size > 100: + page_size = 100 + + qs = Fadan.objects.all() + + # 状态筛选 + zhuangtai = request.data.get('zhuangtai') + if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]: + qs = qs.filter(zhuangtai=int(zhuangtai)) + + # 被处罚人ID + beichufa_id = request.data.get('beichufa_id', '').strip() + if beichufa_id: + qs = qs.filter(beichufa_id=beichufa_id) + + # 被处罚人身份 + beichufa_shenfen = request.data.get('beichufa_shenfen') + if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]: + qs = qs.filter(shenfen=int(beichufa_shenfen)) + + # 模糊搜索(订单ID、被罚人ID、申请人ID) + sousuo = request.data.get('sousuo', '').strip() + if sousuo: + qs = qs.filter( + Q(guanliandingdan_id__icontains=sousuo) | + Q(beichufa_id__icontains=sousuo) | + Q(shenqing_chufa__icontains=sousuo) + ) + + qs = qs.order_by('-create_time') + paginator = Paginator(qs, page_size) + try: + page_obj = paginator.page(page) + except Exception: + return Response({ + 'code': 0, 'msg': '成功', + 'data': {'list': [], 'total': 0, 'page': page, 'page_size': page_size} + }) + + records = list(page_obj) + fadan_ids = [r.id for r in records] + + # 批量获取图片并分组 + zhengju_map = {} + shensu_map = {} + if fadan_ids: + tupians = FadanShensuTupian.objects.filter( + fadan_id__in=fadan_ids + ).values('fadan_id', 'tupian_url', 'yongtu') + for t in tupians: + fid = t['fadan_id'] + url = t['tupian_url'] + if t['yongtu'] == 1: + zhengju_map.setdefault(fid, []).append(url) + else: + shensu_map.setdefault(fid, []).append(url) + + # 组装列表数据 + results = [] + for r in records: + results.append({ + 'id': r.id, + 'beichufa_id': r.beichufa_id, + 'shenfen': r.shenfen, + 'shenfen_text': dict(Fadan._meta.get_field('shenfen').choices).get(r.shenfen, ''), + 'shenqing_chufa': r.shenqing_chufa or '', + 'shenqingren_shenfen': r.shenqingren_shenfen, + 'chufaliyou': r.chufaliyou or '', + 'fakuanjine': str(r.fakuanjine), + 'guanliandingdan_id': r.guanliandingdan_id or '', + 'zhuangtai': r.zhuangtai, + 'yingxiang_qiangdan': r.yingxiang_qiangdan, + 'shensuliyou': r.shensuliyou or '', + 'bohuiliyou': r.bohuiliyou or '', + 'chulizhe': r.chulizhe or '', + 'chulizhe_shenfen': r.chulizhe_shenfen, + 'zhengju_tupian': zhengju_map.get(r.id, []), + 'shensu_tupian': shensu_map.get(r.id, []), + 'create_time': r.create_time.strftime('%Y-%m-%d %H:%M:%S') if r.create_time else '', + 'update_time': r.update_time.strftime('%Y-%m-%d %H:%M:%S') if r.update_time else '', + }) + + return Response({ + 'code': 0, 'msg': '成功', + 'data': { + 'list': results, + 'total': paginator.count, + 'page': page, + 'page_size': page_size, + } + }) + except Exception as e: + logger.error(f"罚款列表异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + +class FaKuanChuLiView(APIView): + """ + 处理罚款申诉(同意/拒绝) + POST /houtai/glyclfk + """ + permission_classes = [IsAuthenticated] + parser_classes = [MultiPartParser, FormParser, JSONParser] + + def post(self, request): + try: + username = request.data.get('username', '').strip() + fadan_id = request.data.get('fadan_id') + action = request.data.get('action', '').strip() + chuli_liyou = request.data.get('chuli_liyou', '').strip() + + if not all([username, fadan_id, action]): + return Response({'code': 400, 'msg': '参数不完整'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + with transaction.atomic(): + try: + fadan = Fadan.objects.select_for_update().get(id=fadan_id) + except Fadan.DoesNotExist: + return Response({'code': 404, 'msg': '罚单不存在'}) + + if fadan.zhuangtai != 3: + return Response({'code': 400, 'msg': '当前状态不可处理'}) + + if not check_fadan_permission(permissions, fadan.shenfen): + return Response({'code': 403, 'msg': '无权限处理该类罚单'}) + + # 处理上传的图片 + uploaded_urls = [] + files = request.FILES.getlist('tupian') or [] + if not files: + single = request.FILES.get('tupian') + if single: + files = [single] + for file in files: + valid, msg = validate_image(file) + if not valid: + return Response({'code': 400, 'msg': msg}) + ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' + file_name = f"chufa_{fadan_id}_{int(time.time() * 1000)}.{ext}" + file_path = f"chufatupian/kechufatupian/{file_name}" + url = upload_to_oss(file, file_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + uploaded_urls.append(file_path) + + # 更新状态 + if action == 'agree': # 同意申诉 → 驳回处罚 + fadan.zhuangtai = 4 + fadan.bohuiliyou = chuli_liyou + elif action == 'reject': # 拒绝申诉 → 回到待缴纳 + fadan.zhuangtai = 1 + fadan.bohuiliyou = chuli_liyou + else: + return Response({'code': 400, 'msg': '无效的操作类型'}) + + # 记录处理者(使用 request.user 的 phone) + fadan.chulizhe = request.user.phone + fadan.chulizhe_shenfen = 2 # 这里假设处理者是售后身份,可根据实际业务获取 + fadan.save() + + # 保存图片(证据图片,用途1) + for relative_url in uploaded_urls: + FadanShensuTupian.objects.create( + fadan_id=fadan_id, + beichufa_id=fadan.beichufa_id, + tupian_url=relative_url, + yongtu=1 + ) + + logger.info(f"罚款处理成功: {fadan_id}, 操作: {action}") + return Response({'code': 0, 'msg': '处理成功'}) + + except Exception as e: + logger.error(f"罚款处理异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + +class FaKuanChuangJianView(APIView): + """ + 管理员创建罚款 + POST /houtai/htfksc + """ + permission_classes = [IsAuthenticated] + parser_classes = [MultiPartParser, FormParser, JSONParser] + + def post(self, request): + try: + username = request.data.get('username', '').strip() + beichufa_id = request.data.get('beichufa_id', '').strip() + shenfen = request.data.get('shenfen') + chufaliyou = request.data.get('chufaliyou', '').strip() + fakuanjine = request.data.get('fakuanjine') + yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) + + if not all([username, beichufa_id, shenfen, chufaliyou, fakuanjine]): + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + shenfen = int(shenfen) + fakuanjine = float(fakuanjine) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': '参数格式错误'}) + + if shenfen not in [1, 2, 3, 4]: + return Response({'code': 400, 'msg': '无效的被罚人身份'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + if not check_fadan_permission(permissions, shenfen): + return Response({'code': 403, 'msg': '无权限创建该类罚单'}) + + # 验证用户是否存在对应的扩展表 + try: + user = UserMain.objects.get(yonghuid=beichufa_id) + profile_attr = SHENFEN_PROFILE_MAP.get(shenfen) + if not profile_attr or not hasattr(user, profile_attr): + return Response({'code': 400, 'msg': '该用户不是所选身份'}) + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + + # 处理图片上传 + uploaded_urls = [] + files = request.FILES.getlist('tupian') or [] + if not files: + single = request.FILES.get('tupian') + if single: + files = [single] + for file in files: + valid, msg = validate_image(file) + if not valid: + return Response({'code': 400, 'msg': msg}) + ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' + file_name = f"chufa_{int(time.time() * 1000)}.{ext}" + file_path = f"chufatupian/kechufatupian/{file_name}" + url = upload_to_oss(file, file_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + uploaded_urls.append(file_path) + + with transaction.atomic(): + fadan = Fadan.objects.create( + beichufa_id=beichufa_id, + shenfen=shenfen, + shenqing_chufa=request.user.phone, # 申请人(客服)手机号 + shenqingren_shenfen=2, # 售后身份 + chufaliyou=chufaliyou, + fakuanjine=fakuanjine, + yingxiang_qiangdan=yingxiang_qiangdan, + zhuangtai=1, # 待缴纳 + ) + for relative_url in uploaded_urls: + FadanShensuTupian.objects.create( + fadan=fadan, + beichufa_id=beichufa_id, + tupian_url=relative_url, + yongtu=1 + ) + + logger.info(f"罚款创建成功: {fadan.id}") + return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': fadan.id}}) + + except Exception as e: + logger.error(f"创建罚款异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + + + +from dengji.models import Bankuai + + + +class HqbkxxView(APIView): + """ + 获取板块配置所需全部数据 + POST /houtai/hqbkxx + Body: { "phone": "管理员手机号" } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 从前端获取传递的账号,用于防越权校验 + username_from_frontend = request.data.get('phone', None) + + # 2. 调用公共验证方法 + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + + # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值 + if kefu_obj is None: + return permissions + + # ---------- 验证通过 ---------- + # 4. 检查是否有板块管理权限 + if 'bankuai' not in permissions: + return Response({'code': 403, 'msg': '无板块管理权限'}, status=403) + + # 5. 查询所有板块 + bankuai_list = [] + for bk in Bankuai.objects.all(): + bankuai_list.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'create_time': bk.create_time.strftime('%Y-%m-%d %H:%M:%S') if bk.create_time else '' + }) + + # 6. 查询所有商品类型 + shangpin_leixing_list = [] + for sp in ShangpinLeixing.objects.all(): + shangpin_leixing_list.append({ + 'id': sp.id, + 'jieshao': sp.jieshao, + 'tupian_url': sp.tupian_url or '', + 'bankuai_id': sp.bankuai_id + }) + + # 7. 查询所有会员 + huiyuan_list = [] + for hy in Huiyuan.objects.all(): + huiyuan_list.append({ + 'huiyuan_id': hy.huiyuan_id, + 'jieshao': hy.jieshao, + 'bankuai_id': hy.bankuai_id + }) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'bankuai': bankuai_list, + 'shangpin_leixing': shangpin_leixing_list, + 'huiyuan': huiyuan_list + } + }) + + +class BkxgView(APIView): + """ + 板块增删改统一接口 + POST /houtai/bkxg + Body: { + "phone": "管理员手机号", + "action": "create | update | delete | add_items | remove_items", + "bankuai_id": int, + "bankuai_name": str, + "item_type": "shangpin_leixing | huiyuan", + "item_ids": [1,2,3] + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 从前端获取传递的账号 + username_from_frontend = request.data.get('phone', None) + + # 2. 调用公共验证方法 + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + + # 3. 验证失败直接返回 + if kefu_obj is None: + return permissions + + # 4. 检查板块管理权限 + if 'bankuai' not in permissions: + return Response({'code': 403, 'msg': '无板块管理权限'}, status=403) + + # 5. 获取操作类型 + action = request.data.get('action') + if not action: + return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400) + + # ==================== 创建板块 ==================== + if action == 'create': + bankuai_name = request.data.get('bankuai_name', '').strip() + if not bankuai_name: + return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400) + if Bankuai.objects.filter(mingcheng=bankuai_name).exists(): + return Response({'code': 1, 'msg': '板块名称已存在'}, status=400) + + shangpin_ids = request.data.get('shangpin_ids', []) + huiyuan_ids = request.data.get('huiyuan_ids', []) + + with transaction.atomic(): + new_bk = Bankuai.objects.create(mingcheng=bankuai_name) + if shangpin_ids: + ShangpinLeixing.objects.filter( + id__in=shangpin_ids, + bankuai__isnull=True + ).update(bankuai=new_bk) + if huiyuan_ids: + Huiyuan.objects.filter( + huiyuan_id__in=huiyuan_ids, + bankuai__isnull=True + ).update(bankuai=new_bk) + + return Response({ + 'code': 0, + 'msg': '创建成功', + 'data': {'bankuai_id': new_bk.bankuai_id} + }) + + # ==================== 以下操作需要 bankuai_id ==================== + bankuai_id = request.data.get('bankuai_id') + if not bankuai_id: + return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400) + + try: + bk = Bankuai.objects.get(bankuai_id=bankuai_id) + except Bankuai.DoesNotExist: + return Response({'code': 1, 'msg': '板块不存在'}, status=404) + + # ==================== 修改板块名称 ==================== + if action == 'update': + new_name = request.data.get('bankuai_name', '').strip() + if not new_name: + return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400) + if Bankuai.objects.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists(): + return Response({'code': 1, 'msg': '板块名称已存在'}, status=400) + bk.mingcheng = new_name + bk.save() + return Response({'code': 0, 'msg': '修改成功'}) + + # ==================== 删除板块 ==================== + elif action == 'delete': + with transaction.atomic(): + ShangpinLeixing.objects.filter(bankuai=bk).update(bankuai=None) + Huiyuan.objects.filter(bankuai=bk).update(bankuai=None) + bk.delete() + return Response({'code': 0, 'msg': '删除成功'}) + + # ==================== 添加绑定 ==================== + elif action == 'add_items': + item_type = request.data.get('item_type') + item_ids = request.data.get('item_ids', []) + if not item_type or not item_ids: + return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400) + + if item_type == 'shangpin_leixing': + ShangpinLeixing.objects.filter( + id__in=item_ids, + bankuai__isnull=True + ).update(bankuai=bk) + elif item_type == 'huiyuan': + Huiyuan.objects.filter( + huiyuan_id__in=item_ids, + bankuai__isnull=True + ).update(bankuai=bk) + else: + return Response({'code': 1, 'msg': '无效的 item_type'}, status=400) + return Response({'code': 0, 'msg': '添加成功'}) + + # ==================== 移除绑定 ==================== + elif action == 'remove_items': + item_type = request.data.get('item_type') + item_ids = request.data.get('item_ids', []) + if not item_type or not item_ids: + return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400) + + if item_type == 'shangpin_leixing': + ShangpinLeixing.objects.filter( + id__in=item_ids, + bankuai=bk + ).update(bankuai=None) + elif item_type == 'huiyuan': + Huiyuan.objects.filter( + huiyuan_id__in=item_ids, + bankuai=bk + ).update(bankuai=None) + else: + return Response({'code': 1, 'msg': '无效的 item_type'}, status=400) + return Response({'code': 0, 'msg': '移除成功'}) + + else: + return Response({'code': 1, 'msg': '未知操作'}, status=400) + + + + + +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView +from rest_framework.response import Response +from django.db.models import Sum +from django.utils import timezone +from datetime import date, timedelta +from peizhi.models import ( + DailyIncomeStat, DailyPayoutStat +) +from shangpin.models import ( + Czjilu +) +from .utils import verify_kefu_permission + + +class CaiwuView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + + if kefu_obj is None: + return permissions + + if 'caiwu' not in permissions: + return Response({'code': 403, 'msg': '无财务查看权限'}, status=403) + + today = date.today() + today_start = datetime.combine(today, datetime.min.time()) + today_end = today_start + timedelta(days=1) + + # 今日收入 + today_income = DailyIncomeStat.objects.filter(date=today).first() + today_income_amount = float(today_income.total_amount) if today_income else 0.00 + today_income_count = today_income.total_count if today_income else 0 + + # 今日支出 + today_payout = DailyPayoutStat.objects.filter(date=today).first() + today_payout_amount = float(today_payout.total_amount) if today_payout else 0.00 + today_payout_count = today_payout.total_count if today_payout else 0 + + # 今日订单(已付款及之后的状态) + today_orders = Dingdan.objects.filter( + create_time__gte=today_start, + create_time__lt=today_end, + zhuangtai__in=[1,2,3,4,5,6,7,8] + ) + today_order_count = today_orders.count() + today_order_amount = float(today_orders.aggregate(total=Sum('jine'))['total'] or 0.00) + + # 今日成交(zhuangtai=3) + today_completed = Dingdan.objects.filter( + create_time__gte=today_start, + create_time__lt=today_end, + zhuangtai=3 + ) + today_completed_count = today_completed.count() + today_completed_amount = float(today_completed.aggregate(total=Sum('jine'))['total'] or 0.00) + + # 今日退款(zhuangtai=5) + today_tuikuan = Dingdan.objects.filter( + create_time__gte=today_start, + create_time__lt=today_end, + zhuangtai=5 + ) + today_tuikuan_count = today_tuikuan.count() + today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('jine'))['total'] or 0.00) + + # 今日充值(会员充值,leixing=1, zhuangtai=3) + today_chongzhi = Czjilu.objects.filter( + create_time__gte=today_start, + create_time__lt=today_end, + leixing=1, + zhuangtai=3 + ) + today_chongzhi_count = today_chongzhi.count() + today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00) + + # 今日新增会员:从购买记录表统计今天创建的用户(去重) + new_huiyuan_user_ids = Huiyuangoumai.objects.filter( + create_time__gte=today_start, + create_time__lt=today_end + ).values_list('yonghu_id', flat=True).distinct() + today_new_huiyuan = len(new_huiyuan_user_ids) + + # 新增会员充值总额(这些用户今天的会员充值金额) + today_new_huiyuan_amount = 0.0 + if new_huiyuan_user_ids: + amount_sum = Czjilu.objects.filter( + create_time__gte=today_start, + create_time__lt=today_end, + yonghuid__in=new_huiyuan_user_ids, + leixing=1, + zhuangtai=3 + ).aggregate(total=Sum('jine')) + today_new_huiyuan_amount = float(amount_sum['total'] or 0.0) + + # 各角色可提现余额总和 + all_guanshi_yue = float(UserGuanshi.objects.aggregate(total=Sum('yue'))['total'] or 0.00) + all_dashou_yue = float(UserDashou.objects.aggregate(total=Sum('yue'))['total'] or 0.00) + all_zuzhang_yue = float(UserZuzhang.objects.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00) + all_shangjia_yue = float(UserShangjia.objects.aggregate(total=Sum('yue'))['total'] or 0.00) + + # 平台总收支及利润 + total_income = float(DailyIncomeStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00) + total_payout = float(DailyPayoutStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00) + platform_profit = round(total_income - total_payout, 2) + + # 近30天每日收支明细 + daily_stats = [] + income_qs = DailyIncomeStat.objects.order_by('-date')[:30] + payout_dict = { + str(p.date): float(p.total_amount) + for p in DailyPayoutStat.objects.filter(date__gte=today - timedelta(days=30)) + } + for inc in income_qs: + d = str(inc.date) + daily_stats.append({ + 'date': d, + 'income': float(inc.total_amount), + 'income_count': inc.total_count, + 'payout': payout_dict.get(d, 0.00), + 'profit': round(float(inc.total_amount) - payout_dict.get(d, 0.00), 2) + }) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'today_income': round(today_income_amount, 2), + 'today_income_count': today_income_count, + 'today_payout': round(today_payout_amount, 2), + 'today_payout_count': today_payout_count, + 'today_order_count': today_order_count, + 'today_order_amount': round(today_order_amount, 2), + 'today_completed_count': today_completed_count, + 'today_completed_amount': round(today_completed_amount, 2), + 'today_tuikuan_count': today_tuikuan_count, + 'today_tuikuan_amount': round(today_tuikuan_amount, 2), + 'today_chongzhi_count': today_chongzhi_count, + 'today_chongzhi_amount': round(today_chongzhi_amount, 2), + 'today_new_huiyuan': today_new_huiyuan, + 'today_new_huiyuan_amount': round(today_new_huiyuan_amount, 2), + 'all_guanshi_yue': round(all_guanshi_yue, 2), + 'all_dashou_yue': round(all_dashou_yue, 2), + 'all_zuzhang_yue': round(all_zuzhang_yue, 2), + 'all_shangjia_yue': round(all_shangjia_yue, 2), + 'total_income': round(total_income, 2), + 'total_payout': round(total_payout, 2), + 'platform_profit': platform_profit, + 'daily_stats': daily_stats + } + }) + + + + + + +class CwhybkhqView(APIView): + """ + 获取所有板块及对应会员列表 + POST /houtai/cwhybkhq + Body: { "phone": "管理员手机号" } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'huiyuancaiwu' not in permissions: + return Response({'code': 403, 'msg': '无财务查看权限'}, status=403) + + # 查询所有板块 + bankuai_list = [] + for bk in Bankuai.objects.all(): + # 获取该板块下所有会员 + members = Huiyuan.objects.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id') + bankuai_list.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'members': list(members) + }) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': bankuai_list + }) + + + +from django.db.models.functions import TruncDate, TruncMonth, TruncYear +from shangpin.models import Czjilu, Gsfenhong +from .utils import verify_kefu_permission + +from django.db.models.functions import TruncDate, TruncMonth, TruncYear +from shangpin.models import Czjilu, Gsfenhong # 请根据实际路径调整 +class HybkjtsjView(APIView): + """ + 会员详细统计数据 + POST /houtai/hybkjtsj + Body: { + "phone": "管理员手机号", + "huiyuan_id": "会员ID", + "granularity": "day" | "month" | "year", + "year": 2026, + "month": 5, + "include_guanshi": true, + "include_zuzhang": true, + "summary_date": "2026-05-16" + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'huiyuancaiwu' not in permissions: + return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403) + + huiyuan_id = request.data.get('huiyuan_id') + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + include_guanshi = request.data.get('include_guanshi', False) + include_zuzhang = request.data.get('include_zuzhang', False) + summary_date = request.data.get('summary_date') + + if not huiyuan_id: + return Response({'code': 1, 'msg': '缺少会员ID'}, status=400) + + # 🔧 直接使用 date.today() 避免模块名冲突 + from datetime import date, datetime, timedelta + now = date.today() + + # ---------- 构造时间范围 ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + start_date = date(year, month, 1) + if month == 12: + end_date = date(year + 1, 1, 1) + else: + end_date = date(year, month + 1, 1) + trunc_func = TruncDate('create_time') + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncMonth('create_time') + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() + elif granularity == 'year': + year = year or now.year + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncYear('create_time') + if not summary_date: + summary_date = str(year) + summary_q = date(year, 1, 1) + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + # 生成 naive datetime(避免 USE_TZ=False 报错) + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date, datetime.min.time()) + + # ---------- 充值过滤 ---------- + chongzhi_filter = { + 'huiyuan_id': huiyuan_id, # 会员ID筛选 + 'leixing': 1, + 'zhuangtai': 3, + 'create_time__gte': start_dt, + 'create_time__lt': end_dt + } + + # ---------- 分红过滤 ---------- + fenhong_filter = { + 'huiyuan_id': huiyuan_id, # 分红表会员ID筛选 + 'create_time__gte': start_dt, + 'create_time__lt': end_dt + } + + # 充值分组查询 + chongzhi_qs = Czjilu.objects.filter(**chongzhi_filter) \ + .annotate(period=trunc_func) \ + .values('period') \ + .annotate( + chongzhi_count=Count('id'), + chongzhi_amount=Sum('jine') + ).order_by('period') + + # 分红分组查询 + fenhong_qs = None + if include_guanshi or include_zuzhang: + fenhong_agg = {} + if include_guanshi: + fenhong_agg['guanshi_amount'] = Sum('fenhong') + if include_zuzhang: + fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong') + fenhong_qs = Gsfenhong.objects.filter(**fenhong_filter) \ + .annotate(period=trunc_func) \ + .values('period') \ + .annotate(**fenhong_agg) \ + .order_by('period') + + # 合并数据 + data_map = {} + for item in chongzhi_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + data_map[period_str] = { + 'chongzhi_count': item['chongzhi_count'], + 'chongzhi_amount': float(item['chongzhi_amount'] or 0) + } + + if fenhong_qs: + for item in fenhong_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + if period_str not in data_map: + data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0} + if include_guanshi: + data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0) + if include_zuzhang: + data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0) + + time_series = [] + for period_str, vals in data_map.items(): + guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0 + zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0 + vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2) + vals['date'] = period_str + time_series.append(vals) + time_series.sort(key=lambda x: x['date']) + + # ---------- 汇总数据 ---------- + summary_chongzhi_filter = chongzhi_filter.copy() + if granularity == 'day': + summary_chongzhi_filter['create_time__date'] = summary_q + elif granularity == 'month': + summary_chongzhi_filter['create_time__year'] = summary_q.year + summary_chongzhi_filter['create_time__month'] = summary_q.month + else: + summary_chongzhi_filter['create_time__year'] = summary_q.year + + summary_chongzhi = Czjilu.objects.filter(**summary_chongzhi_filter).aggregate( + total_count=Count('id'), + total_amount=Sum('jine') + ) + total_chongzhi_count = summary_chongzhi['total_count'] or 0 + total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0) + + total_guanshi = 0.0 + total_zuzhang = 0.0 + if include_guanshi or include_zuzhang: + summary_fenhong_filter = fenhong_filter.copy() + if granularity == 'day': + summary_fenhong_filter['create_time__date'] = summary_q + elif granularity == 'month': + summary_fenhong_filter['create_time__year'] = summary_q.year + summary_fenhong_filter['create_time__month'] = summary_q.month + else: + summary_fenhong_filter['create_time__year'] = summary_q.year + + fenhong_agg = {} + if include_guanshi: + fenhong_agg['guanshi_sum'] = Sum('fenhong') + if include_zuzhang: + fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong') + summary_fenhong = Gsfenhong.objects.filter(**summary_fenhong_filter).aggregate(**fenhong_agg) + total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0) + total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0) + + total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': { + 'total_chongzhi_count': total_chongzhi_count, + 'total_chongzhi_amount': total_chongzhi_amount, + 'total_guanshi_fenhong': total_guanshi, + 'total_zuzhang_fenhong': total_zuzhang, + 'total_shouyi': total_shouyi + } + } + }) + except Exception as e: + logger.exception("HybkjtsjView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + + + +class SzxxView(APIView): + """ + 每日收支详细统计数据 + POST /houtai/szxx + Body: { + "phone": "管理员手机号", + "granularity": "day" | "month" | "year", + "year": 2026, // day/month 时必填 + "month": 5, // day 时必填 + "summary_date": "2026-05-16" // 可选,格式随颗粒度 + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 避免模块名冲突,局部导入 + from datetime import date, datetime + + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'caiwu' not in permissions: + return Response({'code': 403, 'msg': '无财务查看权限'}, status=403) + + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + summary_date = request.data.get('summary_date') + + now = date.today() + + # ---------- 时间范围与分组字段 ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + # 分组字段 + group_field = 'day' + # 过滤条件 + income_filter = {'year': year, 'month': month} + payout_filter = {'year': year, 'month': month} + # 默认汇总日期 + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_parts = summary_date.split('-') + summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1]), 'day': int(summary_parts[2])} + # 用于构造返回的日期字符串 + date_format = '%Y-%m-%d' + + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + group_field = 'month' + income_filter = {'year': year} + payout_filter = {'year': year} + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_parts = summary_date.split('-') + summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1])} + date_format = '%Y-%m' + + elif granularity == 'year': + group_field = 'year' + income_filter = {} + payout_filter = {} + if not summary_date: + summary_date = str(now.year) + summary_filter = {'year': int(summary_date)} + date_format = '%Y' + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + # ---------- 收入分组查询 ---------- + income_qs = DailyIncomeStat.objects.filter(**income_filter) \ + .values(group_field) \ + .annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \ + .order_by(group_field) + + # ---------- 支出分组查询 ---------- + payout_qs = DailyPayoutStat.objects.filter(**payout_filter) \ + .values(group_field) \ + .annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) \ + .order_by(group_field) + + # ---------- 合并数据 ---------- + data_map = {} + for item in income_qs: + key = item[group_field] + data_map[key] = { + 'income': float(item['total_income'] or 0), + 'income_count': item['total_count'] or 0, + 'payout': 0.0, + 'payout_count': 0 + } + + for item in payout_qs: + key = item[group_field] + if key not in data_map: + data_map[key] = {'income': 0.0, 'income_count': 0, 'payout': 0.0, 'payout_count': 0} + data_map[key]['payout'] = float(item['total_payout'] or 0) + data_map[key]['payout_count'] = item['total_count'] or 0 + + time_series = [] + for key, vals in data_map.items(): + # 构造日期字符串 + if granularity == 'day': + date_str = f"{year}-{month:02d}-{key:02d}" + elif granularity == 'month': + date_str = f"{year}-{key:02d}" + else: + date_str = str(key) + profit = round(vals['income'] - vals['payout'], 2) + time_series.append({ + 'date': date_str, + 'income': vals['income'], + 'payout': vals['payout'], + 'profit': profit + }) + time_series.sort(key=lambda x: x['date']) + + # ---------- 汇总数据 ---------- + income_summary = DailyIncomeStat.objects.filter(**summary_filter).aggregate( + total_income=Sum('total_amount'), total_count=Sum('total_count') + ) + payout_summary = DailyPayoutStat.objects.filter(**summary_filter).aggregate( + total_payout=Sum('total_amount'), total_count=Sum('total_count') + ) + total_income = float(income_summary['total_income'] or 0) + total_payout = float(payout_summary['total_payout'] or 0) + total_profit = round(total_income - total_payout, 2) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': { + 'total_income': total_income, + 'total_income_count': income_summary['total_count'] or 0, + 'total_payout': total_payout, + 'total_payout_count': payout_summary['total_count'] or 0, + 'total_profit': total_profit + } + } + }) + + except Exception as e: + logger.exception("SzxxView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + + + + + + +class CwddhqlxView(APIView): + """ + 获取订单类型(商品类型)用于筛选 + POST /houtai/cwddhqlx + Body: { "phone": "管理员手机号" } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'dingdancaiwu' not in permissions: + return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403) + + # 获取非审核状态(shenhezhuangtai=1)的商品类型,只返回ID和名称 + types = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values('id', 'jieshao') + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': list(types) + }) + except Exception as e: + logger.exception("CwddhqlxView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + +class CwhqjtddsjView(APIView): + """ + 订单详细统计数据(修正版) + POST /houtai/cwhqjtddsj + Body: { + "phone": "管理员手机号", + "granularity": "day" | "month" | "year", + "year": 2026, + "month": 5, + "type_id": 1, # 商品类型ID,0表示全部 + "order_source": 1, # 1=平台单,2=商家单,0=全部 + "summary_date": "2026-05-16" + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'dingdancaiwu' not in permissions: + return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403) + + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + type_id = request.data.get('type_id', 0) + order_source = request.data.get('order_source', 0) + summary_date = request.data.get('summary_date') + + now = date.today() + + # ---------- 时间范围 ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + start_date = date(year, month, 1) + end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) + trunc_func = TruncDate('create_time') + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncMonth('create_time') + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() + elif granularity == 'year': + year = year or now.year + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncYear('create_time') + if not summary_date: + summary_date = str(year) + summary_q = date(year, 1, 1) + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date, datetime.min.time()) + + # ---------- 基础过滤 ---------- + filters = { + 'create_time__gte': start_dt, + 'create_time__lt': end_dt, + 'zhuangtai__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态 + } + if int(type_id) != 0: + filters['leixing_id'] = type_id + if int(order_source) in [1, 2]: + filters['fadan_pingtai'] = order_source + + # ---------- 时间序列分组查询 ---------- + qs = Dingdan.objects.filter(**filters) \ + .annotate(period=trunc_func) \ + .values('period') \ + .annotate( + # 订单总量(所有符合条件的状态) + order_count=Count('id'), + # 订单总额 + order_amount=Sum('jine'), + # 成交单量 (zhuangtai=3) + success_count=Count('id', filter=Q(zhuangtai=3)), + # 成交订单总额 + success_amount=Sum('jine', filter=Q(zhuangtai=3)), + # 退款单量 (zhuangtai=5) + refund_count=Count('id', filter=Q(zhuangtai=5)), + # 打手分成总额(仅成交订单) + dashou_fencheng_sum=Sum('dashou_fencheng', filter=Q(zhuangtai=3)), + # 平台成交订单的店铺分红 + dianpu_fenhong_sum=Sum('pingtai_kuozhan__dianpu_shouyi', + filter=Q(zhuangtai=3, fadan_pingtai=1)), + # 成交订单中:平台单金额、商家单金额、各自打手分成 + platform_success_amount=Sum('jine', filter=Q(zhuangtai=3, fadan_pingtai=1)), + merchant_success_amount=Sum('jine', filter=Q(zhuangtai=3, fadan_pingtai=2)), + platform_dashou=Sum('dashou_fencheng', filter=Q(zhuangtai=3, fadan_pingtai=1)), + merchant_dashou=Sum('dashou_fencheng', filter=Q(zhuangtai=3, fadan_pingtai=2)), + ).order_by('period') + + time_series = [] + for item in qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + + # 平台收益计算 + platform_profit = (item['platform_success_amount'] or 0) \ + - (item['platform_dashou'] or 0) \ + - (item['dianpu_fenhong_sum'] or 0) + merchant_profit = (item['merchant_success_amount'] or 0) \ + - (item['merchant_dashou'] or 0) + total_profit = platform_profit + merchant_profit + + time_series.append({ + 'date': period_str, + 'order_count': item['order_count'], + 'order_amount': float(item['order_amount'] or 0), + 'success_count': item['success_count'], + 'success_amount': float(item['success_amount'] or 0), + 'refund_count': item['refund_count'], + 'dashou_fencheng': float(item['dashou_fencheng_sum'] or 0), + 'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0), + 'profit': round(total_profit, 2) + }) + + # ---------- 汇总数据 ---------- + summary_qs = Dingdan.objects.filter(**filters) + total_order_count = summary_qs.count() + total_order_amount = summary_qs.aggregate(s=Sum('jine'))['s'] or 0 + total_success_count = summary_qs.filter(zhuangtai=3).count() + total_success_amount = summary_qs.filter(zhuangtai=3).aggregate(s=Sum('jine'))['s'] or 0 + total_refund_count = summary_qs.filter(zhuangtai=5).count() + total_dashou = summary_qs.filter(zhuangtai=3).aggregate(s=Sum('dashou_fencheng'))['s'] or 0 + total_dianpu_fenhong = summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate( + s=Sum('pingtai_kuozhan__dianpu_shouyi'))['s'] or 0 + + platform_amount = summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(s=Sum('jine'))['s'] or 0 + merchant_amount = summary_qs.filter(zhuangtai=3, fadan_pingtai=2).aggregate(s=Sum('jine'))['s'] or 0 + platform_dashou_sum = \ + summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(s=Sum('dashou_fencheng'))['s'] or 0 + merchant_dashou_sum = \ + summary_qs.filter(zhuangtai=3, fadan_pingtai=2).aggregate(s=Sum('dashou_fencheng'))['s'] or 0 + + profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong + profit_merchant = merchant_amount - merchant_dashou_sum + total_profit = profit_platform + profit_merchant + + summary = { + 'total_order_count': total_order_count, + 'total_order_amount': round(float(total_order_amount), 2), + 'total_success_count': total_success_count, + 'total_success_amount': round(float(total_success_amount), 2), + 'total_refund_count': total_refund_count, + 'total_dashou_fencheng': round(float(total_dashou), 2), + 'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2), + 'total_profit': round(total_profit, 2) + } + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': summary + } + }) + except Exception as e: + logger.exception("CwhqjtddsjView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + + +from dingdan.models import Fadan, FadanFenhong # 罚单和罚款分红表,根据实际路径调整 +import logging +from datetime import date, datetime +from django.db.models import Count, Sum, Q + + + + + +class CwqtczhqView(APIView): + """ + 其他充值/罚款数据统计(非会员充值) + POST /houtai/cwqtczhq + Body: { + "phone": "管理员手机号", + "granularity": "day" | "month" | "year", + "year": 2026, + "month": 5, // day时必填 + "types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款 + "summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY) + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'qtczcaiwu' not in permissions: + return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403) + + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部 + summary_date = request.data.get('summary_date') + + if isinstance(types, list) and len(types) == 0: + types = [2, 3, 4, 5, 6] + + now = date.today() + + # ---------- 时间范围(用于曲线图) ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + start_date = date(year, month, 1) + end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) + trunc_func = TruncDate('create_time') + # 默认汇总日期 + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncMonth('create_time') + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() + elif granularity == 'year': + year = year or now.year + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncYear('create_time') + if not summary_date: + summary_date = str(year) + summary_q = date(year, 1, 1) + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date, datetime.min.time()) + + # ---------- 构建时间序列 ---------- + time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} } + + def ensure_date(ds): + if ds not in time_series_map: + time_series_map[ds] = {} + return time_series_map[ds] + + # 1. 处理充值记录(Czjilu)—— 完全不变 + cz_types = [t for t in types if t in [2, 3, 4, 5]] + if cz_types: + cz_qs = Czjilu.objects.filter( + create_time__gte=start_dt, + create_time__lt=end_dt, + leixing__in=cz_types, + zhuangtai=3 # 只统计支付成功的 + ).annotate( + period=trunc_func + ).values('period', 'leixing').annotate( + total_count=Count('id'), + total_amount=Sum('jine') + ).order_by('period', 'leixing') + + for item in cz_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + lx = item['leixing'] + amount = float(item['total_amount'] or 0) + count = item['total_count'] + # 收益:积分(3)收益为金额本身,其他为0 + profit = amount if lx == 3 else 0.0 + + day_data = ensure_date(period_str) + day_data[str(lx)] = { + 'count': count, + 'amount': amount, + 'profit': profit, + 'fenhong_count': 0, # 充值类型无分红 + 'fenhong_amount': 0.0 + } + + # 2. 处理罚款(Fadan)—— 修改:直接从 Fadan 表获取分红总额和分红笔数 + if 6 in types: + # 使用一次聚合查询,同时获取罚款笔数、罚款金额、分红总额、分红笔数 + penalty_qs = Fadan.objects.filter( + create_time__gte=start_dt, + create_time__lt=end_dt, + zhuangtai=2 # 已缴纳 + ).annotate( + period=trunc_func + ).values('period').annotate( + penalty_count=Count('id'), + penalty_amount=Sum('fakuanjine'), + # 直接使用罚单表中的申请人分红金额字段 + fenhong_amount=Sum('shenqingren_fenhong_jine'), + fenhong_count=Count('id', filter=Q(shenqingren_fenhong_jine__gt=0)) + ).order_by('period') + + for item in penalty_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + day_data = ensure_date(period_str) + penalty_amt = float(item['penalty_amount'] or 0) + fenhong_amt = float(item['fenhong_amount'] or 0) + profit = round(penalty_amt - fenhong_amt, 2) + day_data['6'] = { + 'count': item['penalty_count'], + 'amount': penalty_amt, + 'profit': profit, + 'fenhong_count': item['fenhong_count'], + 'fenhong_amount': fenhong_amt + } + + # 构造有序时间序列列表 + time_series = [] + for period_str in sorted(time_series_map.keys()): + entry = {'date': period_str, 'types': {}} + for type_code in sorted(time_series_map[period_str].keys()): + entry['types'][type_code] = time_series_map[period_str][type_code] + time_series.append(entry) + + # ---------- 汇总指定日期(summary)—— 完全保留原逻辑,仅将罚款部分改为从 Fadan 表直接取分红 ---------- + summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0} + + # 汇总充值类型(完全不变) + for lx in cz_types: + filter_kwargs = { + 'leixing': lx, + 'zhuangtai': 3, + } + if granularity == 'day': + filter_kwargs['create_time__date'] = summary_q + elif granularity == 'month': + filter_kwargs['create_time__year'] = summary_q.year + filter_kwargs['create_time__month'] = summary_q.month + else: + filter_kwargs['create_time__year'] = summary_q.year + agg = Czjilu.objects.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine')) + count = agg['count'] or 0 + amount = float(agg['amount'] or 0) + profit = amount if lx == 3 else 0.0 + summary_result['by_type'][str(lx)] = { + 'count': count, + 'amount': round(amount, 2), + 'profit': round(profit, 2), + 'fenhong_count': 0, + 'fenhong_amount': 0.0 + } + summary_result['total_count'] += count + summary_result['total_amount'] += amount + summary_result['total_profit'] += profit + + # 汇总罚款(修改:从 Fadan 表直接取分红,不再查询 FadanFenhong) + if 6 in types: + penalty_filter = { + 'zhuangtai': 2, + } + if granularity == 'day': + penalty_filter['create_time__date'] = summary_q + elif granularity == 'month': + penalty_filter['create_time__year'] = summary_q.year + penalty_filter['create_time__month'] = summary_q.month + else: + penalty_filter['create_time__year'] = summary_q.year + + # 一次聚合:罚款笔数、金额、分红总额、分红笔数 + agg = Fadan.objects.filter(**penalty_filter).aggregate( + count=Count('id'), + amount=Sum('fakuanjine'), + fenhong_amount=Sum('shenqingren_fenhong_jine'), + fenhong_count=Count('id', filter=Q(shenqingren_fenhong_jine__gt=0)) + ) + penalty_count = agg['count'] or 0 + penalty_amount = float(agg['amount'] or 0) + fh_amount = float(agg['fenhong_amount'] or 0) + fh_count = agg['fenhong_count'] or 0 + profit = round(penalty_amount - fh_amount, 2) + + summary_result['by_type']['6'] = { + 'count': penalty_count, + 'amount': round(penalty_amount, 2), + 'profit': profit, + 'fenhong_count': fh_count, + 'fenhong_amount': round(fh_amount, 2) + } + summary_result['total_count'] += penalty_count + summary_result['total_amount'] += penalty_amount + summary_result['total_profit'] += profit + + summary_result['total_amount'] = round(summary_result['total_amount'], 2) + summary_result['total_profit'] = round(summary_result['total_profit'], 2) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': summary_result + } + }) + except Exception as e: + logger.exception("CwqtczhqView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + +'''class CwqtczhqView(APIView): + """ + 其他充值/罚款数据统计(非会员充值) + POST /houtai/cwqtczhq + Body: { + "phone": "管理员手机号", + "granularity": "day" | "month" | "year", + "year": 2026, + "month": 5, // granularity=day 时必填 + "types": [2,3,4,5] // 充值类型:2=押金,3=积分,4=商家,5=考核;包含 -1 或 [2,3,4,5] 表示全部;罚款始终单独统计(前端控制是否展示) + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'qtczcaiwu' not in permissions: + return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403) + + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + types = request.data.get('types', [2, 3, 4, 5]) # 默认全部非会员类型 + + if isinstance(types, list) and len(types) == 0: + types = [2, 3, 4, 5] + + now = date.today() + + # ---------- 时间范围 ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + start_date = date(year, month, 1) + end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) + trunc_func = TruncDate('create_time') + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncMonth('create_time') + elif granularity == 'year': + year = year or now.year + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncYear('create_time') + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date, datetime.min.time()) + + # ---------- 定义收益计算规则 ---------- + # 商家充值(4)、押金(2)、考核(5):平台收益为 0 + # 积分充值(3):收益 = 充值总额 + # 罚款:收益 = 罚款总额 - 分红总额 + + # 最终返回的时间序列结构: + # { "2026-05-01": { "2": {count, amount, profit}, "3": {...}, ... } } + + time_series_map = {} # date_str -> { type_code: {count, amount, profit} } + + # 公共函数:初始化日期条目 + def ensure_date(ds): + if ds not in time_series_map: + time_series_map[ds] = {} + return time_series_map[ds] + + # ---------- 1. 处理充值记录表(Czjilu) ---------- + cz_types = [t for t in types if t in [2, 3, 4, 5]] # 只取充值类型 + if cz_types: + cz_qs = Czjilu.objects.filter( + create_time__gte=start_dt, + create_time__lt=end_dt, + leixing__in=cz_types, + zhuangtai=3 # 支付成功状态(根据你之前的设置) + ).annotate( + period=trunc_func + ).values('period', 'leixing').annotate( + total_count=Count('id'), + total_amount=Sum('jine') + ).order_by('period', 'leixing') + + for item in cz_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + lx = item['leixing'] + amount = float(item['total_amount'] or 0) + count = item['total_count'] + # 计算收益 + if lx == 3: # 积分 + profit = amount + else: # 商家、押金、考核 + profit = 0.0 + + day_data = ensure_date(period_str) + day_data[str(lx)] = { + 'count': count, + 'amount': amount, + 'profit': profit + } + + # ---------- 2. 处理罚款(Fadan) ---------- + # 罚款统计是否展示由前端 types 是否包含 6 决定?约定 types 数组可以包含 6 代表罚款,或不包含则默认返回罚款数据(由前端决定隐藏) + # 简单处理:只要 types 中包含 6 或不指定时默认返回罚款(因为我们把 types 默认值设为全部,而罚款不是一个 leixing,我们约定前端传递 types 中包含 6 表示需要罚款数据) + # 但为了保持一致性,我们在 types 数组中用 6 代表“罚款”。接口支持 types = [2,3,4,5,6]。 + # 后端根据 types 是否含有 6 来决定是否统计罚款。 + include_penalty = (6 in types) if isinstance(types, list) else True + + if include_penalty: + # 已缴纳罚款记录 + penalty_qs = Fadan.objects.filter( + create_time__gte=start_dt, + create_time__lt=end_dt, + zhuangtai=2 # 已缴纳 + ).annotate( + period=trunc_func + ).values('period').annotate( + penalty_count=Count('id'), + penalty_amount=Sum('fakuanjine') + ).order_by('period') + + # 罚款分红(按创建时间分组) + fenhong_qs = FadanFenhong.objects.filter( + create_time__gte=start_dt, + create_time__lt=end_dt + ).annotate( + period=trunc_func + ).values('period').annotate( + fenhong_count=Count('id'), + fenhong_amount=Sum('ketixian_fenhong') + ).order_by('period') + + # 填充到 time_series_map + for item in penalty_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + day_data = ensure_date(period_str) + # 罚款可能之前已有其他类型数据 + penalty_data = day_data.get('6', {'count': 0, 'amount': 0.0, 'profit': 0.0}) + penalty_data['count'] = item['penalty_count'] + penalty_data['amount'] = float(item['penalty_amount'] or 0) + day_data['6'] = penalty_data + + for item in fenhong_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + day_data = ensure_date(period_str) + pen = day_data.get('6', {'count': 0, 'amount': 0.0, 'profit': 0.0}) + # 分红字段单独存储 + pen['fenhong_count'] = item['fenhong_count'] + pen['fenhong_amount'] = float(item['fenhong_amount'] or 0) + # 计算收益 = 罚款金额 - 分红金额 + pen['profit'] = round(pen.get('amount', 0.0) - pen.get('fenhong_amount', 0.0), 2) + day_data['6'] = pen + + # 确保没有罚款但有分红的日子也有记录(上面已处理) + # 还需要对只有分红没有罚款的日子补全罚款数据 + for period_str, day_data in time_series_map.items(): + if '6' in day_data and 'amount' not in day_data['6']: + day_data['6']['amount'] = 0.0 + day_data['6']['count'] = 0 + day_data['6']['profit'] = round(0.0 - day_data['6'].get('fenhong_amount', 0.0), 2) + + # ---------- 构造有序 time_series 列表 ---------- + time_series = [] + for period_str in sorted(time_series_map.keys()): + entry = {'date': period_str, 'types': {}} + for type_code in sorted(time_series_map[period_str].keys()): + entry['types'][type_code] = time_series_map[period_str][type_code] + time_series.append(entry) + + # ---------- 汇总数据 ---------- + summary = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0} + # 汇总各类型数据 + for type_code in [str(t) for t in types]: + total_count = 0 + total_amount = 0.0 + total_profit = 0.0 + for day in time_series: + if type_code in day['types']: + total_count += day['types'][type_code].get('count', 0) + total_amount += day['types'][type_code].get('amount', 0.0) + total_profit += day['types'][type_code].get('profit', 0.0) + summary['by_type'][type_code] = { + 'count': total_count, + 'amount': round(total_amount, 2), + 'profit': round(total_profit, 2) + } + summary['total_count'] += total_count + summary['total_amount'] += total_amount + summary['total_profit'] += total_profit + + summary['total_amount'] = round(summary['total_amount'], 2) + summary['total_profit'] = round(summary['total_profit'], 2) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': summary + } + }) + + except Exception as e: + logger.exception("CwqtczhqView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)''' + + + + +# ==================== 后端接口 ==================== +# 文件:views.py (添加以下两个视图类) + +from dengji.models import ( + Chenghao, KaoheCishuFeiyong, Bankuai, + YonghuChenghao, ShenheJilu, ShenheGuize, # 可能不需要用到但导入无妨 +) + + + +class KhpzhqView(APIView): + """ + 获取所有考核配置数据(称号列表、板块、费用) + POST /houtai/khpzhq + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核配置权限'}, status=403) + + bankuai_list = list(Bankuai.objects.values('bankuai_id', 'mingcheng')) + + chenghao_qs = Chenghao.objects.prefetch_related('kaohecishufeiyong_set').all() + chenghao_list = [] + for ch in chenghao_qs: + # 获取费用:按次数排序,转为 {cishu: feiyong} 字典 + feiyong_dict = { + item.cishu: float(item.feiyong) + for item in ch.kaohecishufeiyong_set.order_by('cishu') + } + max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0 + + chenghao_list.append({ + 'id': ch.id, + 'mingcheng': ch.mingcheng, + 'leixing': ch.leixing, + 'leixing_name': ch.get_leixing_display(), + 'bankuai_id': ch.bankuai_id, + 'tubiao_url': ch.tubiao_url or '', + 'texiao_miaoshu': ch.texiao_miaoshu or '', + 'create_time': ch.create_time.strftime('%Y-%m-%d %H:%M:%S') if ch.create_time else '', + 'kaohe_guize': ch.kaohe_guize or '', + 'kaioi_jinpai': ch.kaioi_jinpai, + 'feiyong': feiyong_dict, # {1:100.0, 2:200.0} + 'max_cishu': max_cishu + }) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'bankuai': bankuai_list, + 'chenghao': chenghao_list + } + }) + except Exception as e: + logger.exception("KhpzhqView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + +class ChzsgcView(APIView): + """ + 称号(标签)增删改查统一接口 + POST /houtai/chzsgc + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核配置权限'}, status=403) + + action = request.data.get('action') + if not action: + return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400) + + # ========== 创建称号 ========== + if action == 'create': + mingcheng = request.data.get('mingcheng', '').strip() + leixing = request.data.get('leixing', 'dashou') + bankuai_id = request.data.get('bankuai_id') + texiao_miaoshu = request.data.get('texiao_miaoshu', '') + kaohe_guize = request.data.get('kaohe_guize', '') + kaioi_jinpai = request.data.get('kaioi_jinpai', False) + feiyong_list = request.data.get('feiyong_list', []) + + if not mingcheng: + return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) + if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']: + return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) + if Chenghao.objects.filter(mingcheng=mingcheng).exists(): + return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) + + with transaction.atomic(): + ch = Chenghao.objects.create( + mingcheng=mingcheng, + leixing=leixing, + bankuai_id=bankuai_id, + texiao_miaoshu=texiao_miaoshu, + kaohe_guize=kaohe_guize, + kaioi_jinpai=kaioi_jinpai + ) + # 处理费用:前端传来 [{cishu:1, feiyong:100}, ...] + if feiyong_list: + for item in feiyong_list: + cishu = int(item.get('cishu', 1)) + feiyong = float(item.get('feiyong', 0)) + KaoheCishuFeiyong.objects.update_or_create( + chenghao=ch, cishu=cishu, + defaults={'feiyong': feiyong} + ) + else: + # 默认添加第一次免费 + KaoheCishuFeiyong.objects.create(chenghao=ch, cishu=1, feiyong=0) + + return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}}) + + # ========== 修改称号 ========== + elif action == 'update': + ch_id = request.data.get('chenghao_id') + if not ch_id: + return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) + try: + ch = Chenghao.objects.get(id=ch_id) + except Chenghao.DoesNotExist: + return Response({'code': 1, 'msg': '称号不存在'}, status=404) + + mingcheng = request.data.get('mingcheng', ch.mingcheng).strip() + leixing = request.data.get('leixing', ch.leixing) + bankuai_id = request.data.get('bankuai_id', ch.bankuai_id) + texiao_miaoshu = request.data.get('texiao_miaoshu', ch.texiao_miaoshu) + kaohe_guize = request.data.get('kaohe_guize', ch.kaohe_guize) + kaioi_jinpai = request.data.get('kaioi_jinpai', ch.kaioi_jinpai) + + if not mingcheng: + return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) + if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']: + return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) + if Chenghao.objects.filter(mingcheng=mingcheng).exclude(id=ch_id).exists(): + return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) + + ch.mingcheng = mingcheng + ch.leixing = leixing + ch.bankuai_id = bankuai_id + ch.texiao_miaoshu = texiao_miaoshu + ch.kaohe_guize = kaohe_guize + ch.kaioi_jinpai = kaioi_jinpai + ch.save() + + # 如果前端传递了 feiyong_list,则全量替换费用 + feiyong_list = request.data.get('feiyong_list') + if feiyong_list is not None: + with transaction.atomic(): + ch.kaohecishufeiyong_set.all().delete() + for item in feiyong_list: + KaoheCishuFeiyong.objects.create( + chenghao=ch, + cishu=int(item.get('cishu', 1)), + feiyong=float(item.get('feiyong', 0)) + ) + + return Response({'code': 0, 'msg': '修改成功'}) + + # ========== 删除称号 ========== + elif action == 'delete': + ch_id = request.data.get('chenghao_id') + if not ch_id: + return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) + try: + ch = Chenghao.objects.get(id=ch_id) + except Chenghao.DoesNotExist: + return Response({'code': 1, 'msg': '称号不存在'}, status=404) + ch.delete() + return Response({'code': 0, 'msg': '删除成功'}) + + # ========== 添加/修改某次费用 ========== + elif action == 'add_feiyong': + ch_id = request.data.get('chenghao_id') + cishu = request.data.get('cishu') + feiyong = request.data.get('feiyong', 0) + if not ch_id or not cishu: + return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400) + try: + ch = Chenghao.objects.get(id=ch_id) + except Chenghao.DoesNotExist: + return Response({'code': 1, 'msg': '称号不存在'}, status=404) + if int(cishu) < 1: + return Response({'code': 1, 'msg': '次数必须>=1'}, status=400) + KaoheCishuFeiyong.objects.update_or_create( + chenghao=ch, cishu=cishu, + defaults={'feiyong': feiyong} + ) + return Response({'code': 0, 'msg': '添加/修改费用成功'}) + + # ========== 删除某次费用 ========== + elif action == 'remove_feiyong': + ch_id = request.data.get('chenghao_id') + cishu = request.data.get('cishu') + if not ch_id or not cishu: + return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400) + if int(cishu) == 1: + return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400) + try: + ch = Chenghao.objects.get(id=ch_id) + except Chenghao.DoesNotExist: + return Response({'code': 1, 'msg': '称号不存在'}, status=404) + KaoheCishuFeiyong.objects.filter(chenghao=ch, cishu=cishu).delete() + return Response({'code': 0, 'msg': '删除成功'}) + + # ========== 修改某次费用(等价于add) ========== + elif action == 'update_feiyong': + return self.add_feiyong(request) # 复用逻辑 + + else: + return Response({'code': 1, 'msg': '未知操作'}, status=400) + + except Exception as e: + logger.exception("ChzsgcView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + +# ==================== 后端 views.py ==================== +# 请确保已有以下导入,如果没有请在文件头部添加 + +from yonghu.models import ( + UserMain, UserShenheguan +) + +from dengji.models import ( + KaoheguanBankuai, Bankuai +) + + + + + + + +class KhgglView(APIView): + """ + 考核官管理数据接口(列表、筛选、分页) + POST /houtai/khggl + Body: { + "phone": "管理员手机号", + "page": 1, // 可选,默认1 + "page_size": 15, // 可选,默认15 + "yonghuid": "1234567", // 按用户ID筛选,可选 + "zhuangtai": 1, // 账号状态 0=禁用,1=正常,可选 + "shenhe_zhuangtai": 0, // 审核官状态 0=空闲,1=审核中,可选 + "bankuai_name": "王者" // 板块名称模糊筛选,可选 + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) + + # 分页参数 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 15)) + if page < 1: page = 1 + if page_size < 1: page_size = 15 + + # 筛选条件 + filter_yonghuid = request.data.get('yonghuid', '').strip() + filter_zhuangtai = request.data.get('zhuangtai') + filter_shenhe_zhuangtai = request.data.get('shenhe_zhuangtai') + filter_bankuai_name = request.data.get('bankuai_name', '').strip() + + # 基础查询:所有审核官,预加载用户及打手扩展表 + queryset = UserShenheguan.objects.select_related('user__dashou_profile') + + if filter_yonghuid: + queryset = queryset.filter(user__yonghuid=filter_yonghuid) + if filter_zhuangtai is not None: + queryset = queryset.filter(zhuangtai=filter_zhuangtai) + if filter_shenhe_zhuangtai is not None: + queryset = queryset.filter(shenhe_zhuangtai=filter_shenhe_zhuangtai) + + # 如果填写了板块名称,筛选出关联该板块的审核官 + if filter_bankuai_name: + bankuai_ids = Bankuai.objects.filter( + mingcheng__icontains=filter_bankuai_name + ).values_list('bankuai_id', flat=True) + shenheguan_ids = KaoheguanBankuai.objects.filter( + bankuai_id__in=bankuai_ids + ).values_list('kaoheguan_id', flat=True) + queryset = queryset.filter(user__yonghuid__in=shenheguan_ids) + + total = queryset.count() + start = (page - 1) * page_size + end = start + page_size + shenheguan_list = queryset.order_by('-create_time')[start:end] + + # ---------- 预计算每个板块的打手数量 ---------- + bankuai_dashou_count_map = {} + all_banquai = Bankuai.objects.all() + for bk in all_banquai: + tag_ids = Chenghao.objects.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True) + count = YonghuChenghao.objects.filter( + chenghao_id__in=tag_ids, + yonghu__dashou_profile__isnull=False + ).values('yonghu').distinct().count() + bankuai_dashou_count_map[bk.bankuai_id] = count + + # ---------- 新增:板块标签统计 ---------- + bankuai_tags_data = [] + for bk in all_banquai: + tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou') + tag_list = [] + for tag in tags: + user_count = YonghuChenghao.objects.filter(chenghao=tag).values('yonghu').distinct().count() + tag_list.append({ + 'id': tag.id, + 'name': tag.mingcheng, + 'user_count': user_count, + }) + bankuai_tags_data.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'tags': tag_list, + }) + + # 构造返回数据(审核官列表) + data_list = [] + for sg in shenheguan_list: + user = sg.user + # 获取该审核官关联的板块 + bk_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=user.yonghuid).select_related('bankuai') + bankuai_info = [] + for bk in bk_qs: + bid = bk.bankuai.bankuai_id + bankuai_info.append({ + 'bankuai_id': bid, + 'mingcheng': bk.bankuai.mingcheng, + 'dashou_count': bankuai_dashou_count_map.get(bid, 0) + }) + + dashou_nick = user.dashou_profile.nicheng if hasattr(user, 'dashou_profile') and user.dashou_profile else '' + + data_list.append({ + 'yonghuid': user.yonghuid, + 'avatar': user.avatar or '', + 'phone': user.phone or '', + 'zhuangtai': sg.zhuangtai, + 'shenhe_zhuangtai': sg.shenhe_zhuangtai, + 'shenhe_zongshu': sg.shenhe_zongshu, + 'tongguo_zongshu': sg.tongguo_zongshu, + 'yue': float(sg.yue), + 'zonge': float(sg.zonge), + 'create_time': sg.create_time.strftime('%Y-%m-%d %H:%M:%S') if sg.create_time else '', + 'update_time': sg.update_time.strftime('%Y-%m-%d %H:%M:%S') if sg.update_time else '', + 'bankuai': bankuai_info, + 'nicheng': dashou_nick, + 'is_renzheng': sg.is_renzheng, + }) + + all_bankuai = list(Bankuai.objects.values('bankuai_id', 'mingcheng')) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'total': total, + 'page': page, + 'page_size': page_size, + 'list': data_list, + 'all_bankuai': all_bankuai, + 'bankuai_tags': bankuai_tags_data, # 新增板块标签统计 + } + }) + + except Exception as e: + logger.exception("KhgglView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + +class ShgxgsjView(APIView): + """ + 审核官信息修改(余额、状态、板块关联) + POST /houtai/shgxgsj + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) + + yonghuid = request.data.get('yonghuid') + if not yonghuid: + return Response({'code': 1, 'msg': '缺少用户ID'}, status=400) + + try: + shenheguan = UserShenheguan.objects.select_related('user').get(user__yonghuid=yonghuid) + except UserShenheguan.DoesNotExist: + return Response({'code': 1, 'msg': '审核官不存在'}, status=404) + + action = request.data.get('action') + + if action == 'update_info': + if 'yue' in request.data: + shenheguan.yue = float(request.data.get('yue', 0)) + if 'zhuangtai' in request.data: + zt = int(request.data.get('zhuangtai')) + if zt not in [0, 1]: + return Response({'code': 1, 'msg': '无效的账号状态'}, status=400) + shenheguan.zhuangtai = zt + if 'shenhe_zhuangtai' in request.data: + szt = int(request.data.get('shenhe_zhuangtai')) + if szt not in [0, 1]: + return Response({'code': 1, 'msg': '无效的审核官状态'}, status=400) + shenheguan.shenhe_zhuangtai = szt + # 新增认证状态编辑 + if 'is_renzheng' in request.data: + shenheguan.is_renzheng = bool(request.data.get('is_renzheng')) + shenheguan.save() + return Response({'code': 0, 'msg': '修改成功'}) + + elif action == 'add_bankuai': + bankuai_id = request.data.get('bankuai_id') + if not bankuai_id: + return Response({'code': 1, 'msg': '缺少板块ID'}, status=400) + if not Bankuai.objects.filter(bankuai_id=bankuai_id).exists(): + return Response({'code': 1, 'msg': '板块不存在'}, status=400) + if KaoheguanBankuai.objects.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists(): + return Response({'code': 1, 'msg': '已关联该板块'}, status=400) + KaoheguanBankuai.objects.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id) + return Response({'code': 0, 'msg': '板块关联成功'}) + + elif action == 'remove_bankuai': + bankuai_id = request.data.get('bankuai_id') + if not bankuai_id: + return Response({'code': 1, 'msg': '缺少板块ID'}, status=400) + deleted, _ = KaoheguanBankuai.objects.filter( + kaoheguan_id=yonghuid, bankuai_id=bankuai_id + ).delete() + if not deleted: + return Response({'code': 1, 'msg': '未找到该板块关联'}, status=400) + return Response({'code': 0, 'msg': '板块移除成功'}) + + else: + return Response({'code': 1, 'msg': '未知操作'}, status=400) + + except Exception as e: + logger.exception("ShgxgsjView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + + + + + + + + + + + + + + +import secrets +from dingdan.models import Dingdan, DingdanShangjia +from dingdan.tongzhi_tasks import dingdan_guangbo + +from peizhi.models import ShangjiaLianjie + +class ZxkfghdsView(APIView): + """ + 客服更换打手接口 + 路径:POST /houtai/zxkfghds + 功能: + 1. 将当前订单状态改为已退款(5),释放接单打手(若有) + 2. 如需跨平台通知,则调用对方平台退款通知接口 + 3. 基于原订单信息生成全新订单(状态直接为 1,已替老板填写) + 4. 生成新的派单链接,并标记为已使用 + 5. 新链接记录上一个链接ID和上一个订单ID + 6. 【关键】不操作商家余额、退款次数、每日统计,因新订单是替换而非新增扣款 + 权限:需要'003aa'(跨平台管理)权限,与退款接口一致 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # ========== 1. 参数提取 ========== + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + if not username or not dingdan_id: + logger.warning("更换打手参数不完整") + return Response({'code': 400, 'msg': '参数不完整'}) + + # ========== 2. 权限校验(与跨平台退款相同) ========== + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 返回错误响应 + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权处理订单'}) + + current_user = request.user + + # ========== 3. 查询原订单及关联信息 ========== + try: + old_order = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {dingdan_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + + # 仅允许状态为 2(进行中)或 8(结算中)更换打手 + if old_order.zhuangtai not in [1,7,2, 8]: + return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'}) + + # 本接口仅处理商家发单(fadan_pingtai=2) + if old_order.fadan_pingtai != 2: + return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'}) + + # 获取原链接(用于模板ID等) + try: + old_lianjie = ShangjiaLianjie.objects.get(dingdan_id=dingdan_id) + except ShangjiaLianjie.DoesNotExist: + logger.error(f"原链接记录不存在,订单: {dingdan_id}") + return Response({'code': 500, 'msg': '链接数据异常'}) + + # 商家信息(仅用于记录关联,不操作余额) + try: + shangjia_ext = old_order.shangjia_kuozhan + shangjia_id = shangjia_ext.shangjia_id + # 获取商家昵称用于新订单扩展表 + shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_nicheng = shangjia_user.shop_profile.nicheng or f"商家{shangjia_id}" + except (ObjectDoesNotExist, UserShangjia.DoesNotExist): + logger.error(f"商家信息缺失,订单: {dingdan_id}") + return Response({'code': 500, 'msg': '商家数据异常'}) + + # ========== 4. 判断是否需要跨平台退款通知 ========== + is_cross = old_order.is_cross + partner_club_id = old_order.partner_club_id + partner_order_id = old_order.partner_order_id + need_notify_partner = is_cross and partner_club_id and partner_order_id + + # ========== 5. 退款处理(仅打手侧,不碰商家) ========== + with transaction.atomic(): + # 5.1 订单状态改为已退款 + old_order.zhuangtai = 5 + old_order.clkf = current_user.phone + old_order.save() + + # 5.2 处理接单打手(若有) + dashou_id = old_order.jiedan_dashou_id + if dashou_id: + try: + dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 # 退款次数+1 + dashou_profile.zhuangtai = 1 # 设为空闲 + dashou_profile.save() + except (UserMain.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"打手 {dashou_id} 不存在,跳过更新") + + # 5.3 更新退款记录表 + Tuikuanjilu.objects.update_or_create( + dingdan_id=dingdan_id, + defaults={ + 'dashouid': dashou_id or '', + 'qingqiuid': '', + 'chuliid': current_user.phone, + 'liyou': '客服更换打手退款', + 'sqzhuangtai': 1, + 'jine': old_order.jine or 0, + 'dashou_fencheng': old_order.dashou_fencheng or 0, + 'jieshao': old_order.jieshao or '', + 'beizhu': '更换打手自动退款', + 'nicheng': old_order.nicheng or '', + } + ) + + # 5.4 打手每日统计(退款) + if dashou_id: + try: + update_dashou_daily_by_action( + yonghuid=dashou_id, + amount=old_order.dashou_fencheng, + action=3 # 3=退款 + ) + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + + # 5.5 客服处理统计 + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + # 5.6 跨平台退款通知(需要时) + if need_notify_partner: + try: + # 获取我方俱乐部ID + my_club_config = ClubConfig.objects.filter(is_self=1).first() + if not my_club_config: + raise Exception("我方俱乐部配置不存在") + self_club_id = my_club_config.club_id + club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id) + partner_domain = club_rel.partner_domain + + notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk' + payload = { + 'order_id': partner_order_id, + 'club_id': self_club_id, + 'status': 5, + 'reason': '客服更换打手退款' + } + + # 调用对方接口(必须成功,否则整个事务回滚) + resp = requests.post(notify_url, json=payload, timeout=5) + if resp.status_code == 200: + resp_json = resp.json() + if resp_json.get('code') != 0: + transaction.set_rollback(True) + logger.error(f"对方平台退款通知失败: {resp_json.get('msg')}") + return Response({'code': 500, 'msg': f'对方平台通知失败: {resp_json.get("msg")}'}) + else: + transaction.set_rollback(True) + logger.error(f"对方平台接口HTTP错误: {resp.status_code}") + return Response({'code': 500, 'msg': '通知对方平台失败'}) + + # 更新跨平台扩展表(如果存在) + from dingdan.models import CrossPlatformOrderData + CrossPlatformOrderData.objects.update_or_create( + dingdan_id=dingdan_id, + defaults={ + 'partner_order_id': partner_order_id, + 'partner_club_id': partner_club_id, + 'partner_order_status': 5, + } + ) + except Exception as e: + transaction.set_rollback(True) + logger.error(f"通知对方平台异常: {e}") + return Response({'code': 500, 'msg': f'通知对方平台失败: {str(e)}'}) + + # 至此,原订单已退款,事务提交 + + # ========== 6. 生成新订单和链接(不扣商家余额) ========== + try: + with transaction.atomic(): + # ---- 6.1 生成新的订单ID和Token ---- + def generate_dingdan_id(): + timestamp = int(time.time() * 1000) + process_id = os.getpid() % 1000 + random_num = random.randint(1000, 9999) + return f"SJ{timestamp}{process_id:03d}{random_num}" + + def generate_secure_token(dd_id, uid): + timestamp = int(time.time() * 1_000_000) + salt = secrets.token_hex(8) + raw = f"{timestamp}:{dd_id}:{uid}:{salt}" + token = hashlib.sha256(raw.encode()).hexdigest()[:24] + if ShangjiaLianjie.objects.filter(lianjie_token=token).exists(): + return generate_secure_token(dd_id, uid) + return token + + def generate_link_url(token): + base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/') + return f"{base}/order/{token}" + + new_dingdan_id = generate_dingdan_id() + secure_token = generate_secure_token(new_dingdan_id, shangjia_id) + + # ---- 6.2 创建新订单(复制核心数据,状态直接为1) ---- + new_order = Dingdan.objects.create( + dingdan_id=new_dingdan_id, + zhuangtai=1, # 下单中 + fadan_pingtai=2, + jine=old_order.jine, + dashou_fencheng=old_order.dashou_fencheng, + leixing_id=old_order.leixing_id, + yaoqiuleixing=old_order.yaoqiuleixing, + huiyuan_id=old_order.huiyuan_id, + yongjin=old_order.yongjin, + jieshao=old_order.jieshao, + nicheng=old_order.nicheng, # 继承游戏昵称 + beizhu=old_order.beizhu, + user1_id=old_order.user1_id, # 商家标识 + tupian=old_order.tupian, + # 注意:不继承 zhiding_id、jiedan_dashou_id + ) + + # ---- 6.3 创建商家扩展表 ---- + DingdanShangjia.objects.create( + dingdan=new_order, + shangjia_id=shangjia_id, + sjnicheng=shangjia_nicheng, + ) + + # ---- 6.4 生成新链接并关联旧链接信息 ---- + link_url = generate_link_url(secure_token) + new_lianjie = ShangjiaLianjie.objects.create( + yonghu_id=shangjia_id, + leixing_id=old_lianjie.leixing_id, + moban_id=old_lianjie.moban_id, + lianjie=link_url, + lianjie_token=secure_token, + dingdan_id=new_dingdan_id, + is_used=True, # 直接标记已使用 + youxiao_shijian=timezone.now() + timezone.timedelta(days=1), + previous_lianjie_id=old_lianjie.id, # 记录旧链接ID + previous_dingdan_id=old_order.dingdan_id, # 记录旧订单ID + ) + + # ---- 6.5 异步通知(广播新订单、跨平台同步) ---- + try: + order_info = { + 'dingdan_id': new_dingdan_id, + 'game_type': self._get_game_type_name(new_order.leixing_id), + 'amount': str(new_order.jine), + 'order_desc': (new_order.jieshao or '')[:50], + } + dingdan_guangbo.delay(order_info) + except Exception as e: + logger.error(f"广播通知失败: {e}") + + # 跨平台同步(与客户填写接口保持一致) + try: + from dingdan.utils import sync_order_to_partners + sync_order_to_partners(new_dingdan_id) + except Exception as e: + logger.error(f"跨平台同步失败: {e}") + + # 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单 + + logger.info(f"更换打手成功:旧订单{dingdan_id}→退款,新订单{new_dingdan_id}已生成") + + return Response({ + 'code': 0, + 'msg': '更换成功', + 'data': { + 'new_dingdan_id': new_dingdan_id, + 'new_lianjie': link_url, + } + }) + + except Exception as e: + logger.error(f"生成新订单事务失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '生成新订单失败'}) + + def _get_game_type_name(self, leixing_id): + """根据类型ID获取游戏类型名称""" + try: + from shangpin.models import ShangpinLeixing + gt = ShangpinLeixing.objects.filter(id=leixing_id).first() + return gt.jieshao if gt else '游戏订单' + except Exception: + return '游戏订单' + + + + + + + +import logging + +from django.conf import settings +from django.db.models import ObjectDoesNotExist +from rest_framework import status +from rest_framework.parsers import JSONParser +from rest_framework.response import Response +from rest_framework.views import APIView + +from dingdan.models import Dingdan + +logger = logging.getLogger('houtai') + + +class KptxwztjbView(APIView): + """ + 跨平台订单状态同步接口(对方平台调用) + 请求:POST /houtai/kptxwztjb + 参数: + - token: 同步专用令牌(明文) + - partner_order_id: 我方订单ID(在对方眼中是partner_order_id,实际查本库dingdan_id) + - our_order_id: 对方订单ID(在本库中为partner_order_id) + - club_id: 俱乐部ID(可选验证) + 返回: + code=0 成功,data.status 为订单状态 + 其他 code 为错误 + 注意:此接口仅用于查询对方订单状态,不做任何修改 + """ + + authentication_classes = [] # 无需JWT等认证 + permission_classes = [] # 公开接口 + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + token = request.data.get('token', '').strip() + partner_order_id = request.data.get('partner_order_id', '').strip() + our_order_id = request.data.get('our_order_id', '').strip() + club_id = request.data.get('club_id', '').strip() + + if not all([token, partner_order_id, our_order_id]): + logger.warning("跨平台状态同步参数缺失") + return Response({'code': 400, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) + + # 2. 验证同步令牌 + SYNC_TOKEN = getattr(settings, 'CROSS_PLATFORM_SYNC_TOKEN', 'xK9mP2vL8qW5rT7y') + if token != SYNC_TOKEN: + logger.warning(f"无效的同步令牌: {token[:8]}...") + return Response({'code': 403, 'msg': '无效令牌'}, status=status.HTTP_403_FORBIDDEN) + + # 3. 查询订单 + # partner_order_id 在对方视角是“我方订单ID”,在本库中应匹配 dingdan_id + try: + order = Dingdan.objects.get(dingdan_id=partner_order_id) + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在, partner_order_id: {partner_order_id}") + return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 4. 验证订单关联关系:本库中的 partner_order_id 应等于传入的 our_order_id + if order.partner_order_id != our_order_id: + logger.warning( + f"订单关联不匹配: partner_order_id={order.partner_order_id}, our_order_id={our_order_id}" + ) + return Response({'code': 400, 'msg': '订单关联信息不匹配'}, status=status.HTTP_400_BAD_REQUEST) + + + + # 6. 返回订单状态 + status_code = order.zhuangtai + logger.info( + f"跨平台状态查询成功, dingdan_id={order.dingdan_id}, status={status_code}" + ) + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'dingdan_id': order.dingdan_id, + 'status': status_code, + } + }) \ No newline at end of file diff --git a/images/dingdan.png b/images/dingdan.png new file mode 100644 index 0000000..b9a3a9c Binary files /dev/null and b/images/dingdan.png differ diff --git a/images/fenlei.png b/images/fenlei.png new file mode 100644 index 0000000..4753795 Binary files /dev/null and b/images/fenlei.png differ diff --git a/images/gonggao.png b/images/gonggao.png new file mode 100644 index 0000000..19cc52c Binary files /dev/null and b/images/gonggao.png differ diff --git a/images/goumai.png b/images/goumai.png new file mode 100644 index 0000000..a2ec8ed Binary files /dev/null and b/images/goumai.png differ diff --git a/images/jiedanchi.png b/images/jiedanchi.png new file mode 100644 index 0000000..16e2b9f Binary files /dev/null and b/images/jiedanchi.png differ diff --git a/images/jieshao.png b/images/jieshao.png new file mode 100644 index 0000000..c713f53 Binary files /dev/null and b/images/jieshao.png differ diff --git a/images/jinpaidashou.png b/images/jinpaidashou.png new file mode 100644 index 0000000..b54a971 Binary files /dev/null and b/images/jinpaidashou.png differ diff --git a/images/role-switch.png b/images/role-switch.png new file mode 100644 index 0000000..bbd4afe Binary files /dev/null and b/images/role-switch.png differ diff --git a/images/shuaxin.png b/images/shuaxin.png new file mode 100644 index 0000000..15ad08d Binary files /dev/null and b/images/shuaxin.png differ diff --git a/images/sousuo.png b/images/sousuo.png new file mode 100644 index 0000000..2a8f235 Binary files /dev/null and b/images/sousuo.png differ diff --git a/images/tiaoxuan.png b/images/tiaoxuan.png new file mode 100644 index 0000000..ffcf298 Binary files /dev/null and b/images/tiaoxuan.png differ diff --git a/images/tuichu.png b/images/tuichu.png new file mode 100644 index 0000000..01553b5 Binary files /dev/null and b/images/tuichu.png differ diff --git a/images/weixinzhifu.png b/images/weixinzhifu.png new file mode 100644 index 0000000..f2551fd Binary files /dev/null and b/images/weixinzhifu.png differ diff --git a/images/wode.png b/images/wode.png new file mode 100644 index 0000000..1fec6b3 Binary files /dev/null and b/images/wode.png differ diff --git a/images/xiaoxi.png b/images/xiaoxi.png new file mode 100644 index 0000000..e6cda27 Binary files /dev/null and b/images/xiaoxi.png differ diff --git a/images/yituikuan.png b/images/yituikuan.png new file mode 100644 index 0000000..62c8fee Binary files /dev/null and b/images/yituikuan.png differ diff --git a/images/yiwancheng.png b/images/yiwancheng.png new file mode 100644 index 0000000..e1c333e Binary files /dev/null and b/images/yiwancheng.png differ diff --git a/images/zhuanqu.png b/images/zhuanqu.png new file mode 100644 index 0000000..2cb7945 Binary files /dev/null and b/images/zhuanqu.png differ diff --git a/images/zhuye.png b/images/zhuye.png new file mode 100644 index 0000000..b2fe9ae Binary files /dev/null and b/images/zhuye.png differ diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..df91779 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'a_long_dianjing.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b3d9125 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "a_long_dianjing", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{} diff --git a/pages/cfss/cfss/cfss.js b/pages/cfss/cfss/cfss.js new file mode 100644 index 0000000..abdec80 --- /dev/null +++ b/pages/cfss/cfss/cfss.js @@ -0,0 +1,100 @@ +// pages/cfss/cfss.js +import request from '../../../utils/request.js'; // 向上4级就是根目录 +// 如果有 COS: + +const app = getApp(); + +Page({ + data: { + // 统计数字(由接口 /yonghu/cffktjhq 返回) + stats: { + fakuan: { total: 0, daijiaona: 0, shensuzhong: 0, yijiaona: 0, yibohui: 0 }, + jifen: { total: 0, daichuli: 0, yichuli: 0, daichuli_0: 0, shensuzhong_3: 0, yichufa_1: 0, yibohui_2: 0 } + }, + + // 当前选中的主标签:'fakuan' 或 'jifen' + activeTab: 'fakuan', + + // 子标签状态值 + activeFakuanStatus: 1, // 罚款默认展示“待缴纳” + activeJifenStatus: 0, // 积分默认展示“待处理” + + // 搜索关键词(订单号) + searchDingdan: '', + + // 刷新触发器,传给子组件 + trigger: { + fakuan: 0, + jifen: 0 + } + }, + + onLoad() { + this.fetchTongji(); + }, + + onShow() { + // 每次显示页面都重新拉取统计,确保数字最新 + this.fetchTongji(); + }, + + // 获取综合统计 + async fetchTongji() { + try { + const res = await request({ + url: '/yonghu/cffktjhq', + method: 'POST' + }); + if (res.statusCode === 200 && res.data.code === 0) { + this.setData({ stats: res.data.data }); + } + } catch (error) { + console.error('获取处罚统计失败', error); + } + }, + + // 切换主Tab(罚款 / 积分) + switchMainTab(e) { + const tab = e.currentTarget.dataset.tab; + if (tab === this.data.activeTab) return; + this.setData({ activeTab: tab }); + }, + + // 切换罚款状态子标签 + switchFakuanStatus(e) { + const status = Number(e.currentTarget.dataset.status); + if (status === this.data.activeFakuanStatus) return; + this.setData({ activeFakuanStatus: status }); + this.bumpTrigger('fakuan'); // 通知罚款列表组件刷新 + }, + + // 切换积分状态子标签 + switchJifenStatus(e) { + const status = Number(e.currentTarget.dataset.status); + if (status === this.data.activeJifenStatus) return; + this.setData({ activeJifenStatus: status }); + this.bumpTrigger('jifen'); // 通知积分列表组件刷新 + }, + + // 搜索输入 + onSearchInput(e) { + this.setData({ searchDingdan: e.detail.value }); + }, + + // 执行搜索(按键盘搜索按钮或点击搜索图标) + onSearchConfirm() { + this.bumpTrigger(this.data.activeTab); + }, + + // 清空搜索关键字 + clearSearch() { + this.setData({ searchDingdan: '' }); + this.bumpTrigger(this.data.activeTab); + }, + + // 递增触发器,子组件通过 observer 响应 + bumpTrigger(tab) { + const key = `trigger.${tab}`; + this.setData({ [key]: this.data.trigger[tab] + 1 }); + } +}); \ No newline at end of file diff --git a/pages/cfss/cfss/cfss.json b/pages/cfss/cfss/cfss.json new file mode 100644 index 0000000..4ca406b --- /dev/null +++ b/pages/cfss/cfss/cfss.json @@ -0,0 +1,10 @@ +{ + "usingComponents": { + "fakuan-list": "/pages/cfss/components/fakuan-list/fakuan-list", + "jifen-list": "/pages/cfss/components/jifen-list/jifen-list" + }, + "navigationBarTitleText": "处罚中心", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTextStyle": "black", + "backgroundColor": "#f7f8fa" + } \ No newline at end of file diff --git a/pages/cfss/cfss/cfss.wxml b/pages/cfss/cfss/cfss.wxml new file mode 100644 index 0000000..03e8517 --- /dev/null +++ b/pages/cfss/cfss/cfss.wxml @@ -0,0 +1,68 @@ + + + + + + + {{ stats.fakuan.daijiaona }} + 待缴罚款 + + + {{ stats.jifen.daichuli }} + 待处理积分 + + + + 罚款:已缴{{ stats.fakuan.yijiaona }} · 申诉中{{ stats.fakuan.shensuzhong }} · 成功{{ stats.fakuan.yibohui }} + 积分:已处理{{ stats.jifen.yichuli }} + + + + + + + 罚款 + + + 积分处罚 + + + + + + + 待缴纳 {{ stats.fakuan.daijiaona }} + 申诉中 {{ stats.fakuan.shensuzhong }} + 已缴纳 {{ stats.fakuan.yijiaona }} + 申诉成功 {{ stats.fakuan.yibohui }} + + + + + 待处理 {{ stats.jifen.daichuli }} + 已处理 {{ stats.jifen.yichuli }} + + + + + + + 搜索 + + + + + + + + + + + \ No newline at end of file diff --git a/pages/cfss/cfss/cfss.wxss b/pages/cfss/cfss/cfss.wxss new file mode 100644 index 0000000..4cd5d19 --- /dev/null +++ b/pages/cfss/cfss/cfss.wxss @@ -0,0 +1,121 @@ +/* pages/cfss/cfss.wxss */ +page { background: #f7f8fa; font-family: -apple-system, BlinkMacSystemFont, sans-serif; } + +.page-container { padding-bottom: 30rpx; } + +/* 统计卡片 */ +.stats-card { + margin: 24rpx 28rpx; + padding: 30rpx 24rpx; + background: #fff; + border-radius: 24rpx; + box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04); +} +.stats-number-row { + display: flex; + justify-content: space-around; + margin-bottom: 20rpx; +} +.number-block { + display: flex; + flex-direction: column; + align-items: center; +} +.big-num { + font-size: 56rpx; + font-weight: 700; +} +.green { color: #2E7D32; } +.orange { color: #E67E22; } +.num-label { + font-size: 24rpx; + color: #888; + margin-top: 8rpx; +} +.stats-detail-row { + border-top: 1rpx solid #eee; + padding-top: 18rpx; +} +.stats-detail-row text { + display: block; + font-size: 22rpx; + color: #aaa; + margin-bottom: 4rpx; +} + +/* 主Tab */ +.tab-row { + display: flex; + margin: 16rpx 28rpx; + background: #fff; + border-radius: 18rpx; + overflow: hidden; + box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.02); +} +.tab-item { + flex: 1; + text-align: center; + padding: 24rpx 0; + font-size: 28rpx; + color: #666; + transition: 0.2s; +} +.tab-item.active { + color: #2E7D32; + font-weight: 600; + background: #F0F9F0; +} + +/* 子状态标签 */ +.sub-tabs { + white-space: nowrap; + margin: 8rpx 28rpx 16rpx; +} +.sub-tag { + display: inline-block; + padding: 10rpx 26rpx; + margin-right: 14rpx; + background: #fff; + border-radius: 30rpx; + font-size: 24rpx; + color: #888; + box-shadow: 0 1rpx 4rpx rgba(0,0,0,0.02); +} +.sub-tag.sub-active { + background: #E8F5E9; + color: #2E7D32; + font-weight: 600; +} + +/* 搜索栏 */ +.search-bar { + display: flex; + align-items: center; + margin: 16rpx 28rpx; + background: #fff; + border-radius: 20rpx; + padding: 12rpx 22rpx; + box-shadow: 0 1rpx 6rpx rgba(0,0,0,0.02); +} +.search-input { + flex: 1; + font-size: 26rpx; + color: #333; +} +.search-btn { + margin-left: 18rpx; + padding: 12rpx 30rpx; + background: #2E7D32; + color: #fff; + border-radius: 16rpx; + font-size: 24rpx; +} +.clear-btn { + margin-left: 16rpx; + font-size: 30rpx; + color: #aaa; +} + +.list-area { + margin-top: 10rpx; +} \ No newline at end of file diff --git a/pages/cfss/components/fakuan-list/fakuan-list.js b/pages/cfss/components/fakuan-list/fakuan-list.js new file mode 100644 index 0000000..cf45977 --- /dev/null +++ b/pages/cfss/components/fakuan-list/fakuan-list.js @@ -0,0 +1,317 @@ +// components/fakuan-list/fakuan-list.js +import request from '../../../../utils/request.js'; + +const app = getApp(); +const MEIYE_TIAOSHU = 5; + +Component({ + properties: { + status: { type: Number, value: 1, observer: 'reload' }, + searchDingdan: { type: String, value: '', observer: 'reload' }, + trigger: { type: Number, value: 0, observer: 'reload' } + }, + + data: { + list: [], + page: 1, + hasMore: true, + loading: false, + loadingMore: false, + ossImageUrl: app.globalData.ossImageUrl || '', + + // 详情弹窗 + showDetail: false, + detailItem: null, + + // 申诉弹窗 + showShensu: false, + shensuLiyou: '', + shensuTupian: [], + uploading: false, + uploadProgress: { current: 0, total: 0 }, + uploadProgressWidth: '0%', + + // 支付弹窗 + showPay: false, + payFadan: null + }, + + lifetimes: { + attached() { this.reload(); } + }, + + methods: { + // ==================== 数据加载 ==================== + reload() { + this.setData({ page: 1, list: [], hasMore: true }); + this.loadData(); + }, + + async loadData(isLoadMore = false) { + // 防止重复请求 + if (this.data.loading || this.data.loadingMore) return; + + if (!isLoadMore) { + this.setData({ loading: true }); + } else { + // 加载更多时,如果后端已经告知没有更多数据,不发送请求 + if (!this.data.hasMore) { + wx.showToast({ title: '没有更多了', icon: 'none', duration: 1000 }); + return; + } + this.setData({ loadingMore: true }); + } + + try { + const res = await request({ + url: '/yonghu/dsfklbhq', + method: 'POST', + data: { + page: this.data.page, + page_size: MEIYE_TIAOSHU, + zhuangtai: this.data.status, + sousuo_dingdan_id: this.data.searchDingdan + } + }); + if (res.statusCode === 200 && res.data.code === 0) { + const data = res.data.data; + const newList = isLoadMore ? this.data.list.concat(data.list || []) : (data.list || []); + this.setData({ + list: newList, + hasMore: data.has_more || false, // 以后端返回为准 + page: isLoadMore ? this.data.page + 1 : 2, // 首次加载后页码为2 + loading: false, + loadingMore: false + }); + } else { + wx.showToast({ title: res.data?.msg || '加载失败', icon: 'none' }); + this.setData({ loading: false, loadingMore: false }); + } + } catch (e) { + wx.showToast({ title: '网络请求失败', icon: 'none' }); + this.setData({ loading: false, loadingMore: false }); + } + }, + + loadMore() { + if (this.data.list.length === 0) return; + this.loadData(true); + }, + + // ==================== 详情弹窗 ==================== + openDetail(e) { + const item = e.currentTarget.dataset.item; + this.setData({ showDetail: true, detailItem: item }); + }, + closeDetail() { + this.setData({ showDetail: false, detailItem: null }); + }, + + // ==================== 申诉逻辑 ==================== + openShensu() { + const item = this.data.detailItem; + if (item.bohuiliyou) { + wx.showToast({ title: '该罚单申诉已被驳回', icon: 'none' }); + return; + } + const existingImgs = (item.shensu_tupian || []).map(url => this.getFullUrl(url)); + this.setData({ + showShensu: true, + shensuLiyou: item.shensuliyou || '', + shensuTupian: existingImgs, + uploading: false, + uploadProgress: { current: 0, total: 0 }, + uploadProgressWidth: '0%' + }); + }, + closeShensu() { + this.setData({ showShensu: false }); + }, + onShensuInput(e) { + this.setData({ shensuLiyou: e.detail.value.slice(0, 500) }); + }, + + chooseImage() { + const remain = 9 - this.data.shensuTupian.length; + if (remain <= 0) return; + wx.chooseMedia({ + count: remain, + mediaType: ['image'], + sourceType: ['album', 'camera'], + success: (res) => { + const newImgs = [...this.data.shensuTupian, ...res.tempFiles.map(f => f.tempFilePath)]; + this.setData({ shensuTupian: newImgs.slice(0, 9) }); + } + }); + }, + deleteShensuImg(e) { + const arr = this.data.shensuTupian; + arr.splice(e.currentTarget.dataset.index, 1); + this.setData({ shensuTupian: arr }); + }, + previewImage(e) { + wx.previewImage({ current: e.currentTarget.dataset.url, urls: [e.currentTarget.dataset.url] }); + }, + previewLocalImg(e) { + const idx = e.currentTarget.dataset.index; + wx.previewImage({ current: this.data.shensuTupian[idx], urls: this.data.shensuTupian }); + }, + + // ==================== 提交流程 ==================== + async submitShensu() { + if (!this.data.shensuLiyou.trim()) { + wx.showToast({ title: '请输入申诉理由', icon: 'none' }); + return; + } + wx.showLoading({ title: '提交中...' }); + try { + const newLocalPaths = this.data.shensuTupian.filter(path => !path.startsWith('http')); + let finalUrls = []; + if (newLocalPaths.length > 0) { + finalUrls = await this.piliangShangchuanShensuTupian(newLocalPaths); + if (!finalUrls) { + wx.hideLoading(); + return; + } + } + const oldRemoteRelative = this.data.shensuTupian + .filter(path => path.startsWith('http')) + .map(url => url.replace(this.data.ossImageUrl, '').replace(/^\//, '')); + const allUrls = oldRemoteRelative.concat(finalUrls); + await this.doSubmitShensu(allUrls); + } catch (err) { + console.error('提交申诉失败', err); + wx.showToast({ title: err.message || '提交失败', icon: 'none' }); + } finally { + wx.hideLoading(); + } + }, + + async doSubmitShensu(tupianUrls) { + const res = await request({ + url: '/yonghu/fkss', + method: 'POST', + data: { + fadan_id: this.data.detailItem.id, + shensu_liyou: this.data.shensuLiyou, + shensu_tupian_urls: tupianUrls + } + }); + if (res.statusCode === 200 && res.data.code === 0) { + wx.showToast({ title: '申诉已提交', icon: 'success' }); + this.setData({ showShensu: false, showDetail: false }); + this.reload(); + } else { + throw new Error(res.data?.msg || '提交失败'); + } + }, + + // ==================== 图片上传到COS ==================== + async piliangShangchuanShensuTupian(localPaths) { + const total = localPaths.length; + if (!total) return []; + this.setData({ uploading: true, uploadProgress: { current: 0, total: total }, uploadProgressWidth: '0%' }); + + let tokenData; + try { + const credRes = await request({ + url: '/dingdan/dsscpz', + method: 'POST', + data: { + dingdan_id: this.data.detailItem.guanliandingdan_id || '', + fadan_id: this.data.detailItem.id, + yongtu: 'fakuan' + } + }); + if (credRes.data.code !== 0) throw new Error(credRes.data.msg || '凭证获取失败'); + tokenData = credRes.data.data; + } catch (e) { + wx.showToast({ title: '获取上传凭证失败', icon: 'none' }); + this.setData({ uploading: false }); + return null; + } + + const COS = require('../../../../utils/cos-wx-sdk-v5.js'); + const credentials = tokenData.credentials || tokenData; + const cos = new COS({ + SimpleUploadMethod: 'putObject', + getAuthorization: (options, callback) => { + callback({ + TmpSecretId: credentials.tmpSecretId, + TmpSecretKey: credentials.tmpSecretKey, + SecurityToken: credentials.sessionToken || '', + StartTime: tokenData.startTime, + ExpiredTime: tokenData.expiredTime + }); + } + }); + const bucket = tokenData.bucket || 'julebu-1361527063'; + const region = tokenData.region || 'ap-shanghai'; + + const yonghuid = app.globalData.userInfo?.yonghuid || wx.getStorageSync('uid') || '0000000'; + const fadanId = this.data.detailItem.id; + const keys = []; + for (let i = 0; i < total; i++) { + keys.push(`fakuan/dashoufakuan/dashoufakuanshensu/${yonghuid}_${fadanId}_${Date.now() + i}.jpg`); + } + + for (let i = 0; i < total; i++) { + try { + await new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => resolve(), 2000); + cos.uploadFile({ + Bucket: bucket, + Region: region, + Key: keys[i], + FilePath: localPaths[i], + onProgress: (info) => { + if (Math.round(info.percent * 100) === 100) { + clearTimeout(timeoutId); + resolve(); + } + } + }, (err) => { + clearTimeout(timeoutId); + if (err) reject(err); + else resolve(); + }); + }); + } catch (err) { + wx.showToast({ title: `第 ${i + 1} 张上传失败`, icon: 'none' }); + this.setData({ uploading: false }); + return null; + } + const done = i + 1; + this.setData({ + uploadProgress: { current: done, total: total }, + uploadProgressWidth: `${((done / total) * 100).toFixed(0)}%` + }); + } + this.setData({ uploading: false }); + return keys; + }, + + // ==================== 缴纳支付 ==================== + startPay() { + this.setData({ + showDetail: false, + showPay: true, + payFadan: this.data.detailItem + }); + }, + onPayClose() { + this.setData({ showPay: false, payFadan: null }); + }, + onPaySuccess() { + this.setData({ showPay: false, payFadan: null }); + this.reload(); + }, + + // ==================== 工具方法 ==================== + getFullUrl(url) { + if (!url) return ''; + if (url.startsWith('http')) return url; + return this.data.ossImageUrl + url; + } + } +}); \ No newline at end of file diff --git a/pages/cfss/components/fakuan-list/fakuan-list.json b/pages/cfss/components/fakuan-list/fakuan-list.json new file mode 100644 index 0000000..d192c45 --- /dev/null +++ b/pages/cfss/components/fakuan-list/fakuan-list.json @@ -0,0 +1,6 @@ +{ + "component": true, + "usingComponents": { + "fakuan-pay": "/pages/cfss/components/fakuan-pay/fakuan-pay" + } +} \ No newline at end of file diff --git a/pages/cfss/components/fakuan-list/fakuan-list.wxml b/pages/cfss/components/fakuan-list/fakuan-list.wxml new file mode 100644 index 0000000..257802a --- /dev/null +++ b/pages/cfss/components/fakuan-list/fakuan-list.wxml @@ -0,0 +1,116 @@ + + + + 加载中... + + + + {{ item.create_time }} + {{ item.display_status }} + + 金额:¥{{ item.fakuanjine }} + 理由:{{ item.chufaliyou }} + 订单:{{ item.guanliandingdan_id }} + + + + + + 点击获取更多 + + 加载中... + —— 没有更多了 —— + 暂无罚款记录 + + + + + + + 罚款详情 + + + + 罚款金额¥{{ detailItem.fakuanjine }} + 当前状态{{ detailItem.display_status }} + 处罚理由{{ detailItem.chufaliyou }} + 关联订单{{ detailItem.guanliandingdan_id }} + 驳回理由{{ detailItem.bohuiliyou }} + 申诉理由{{ detailItem.shensuliyou }} + + + + 证据图片 + + + + + + + + + + + 申诉图片 + + + + + + + + + + + 申诉 + 立即缴纳 + 关闭 + + + + + + + + + 提交申诉 + + + + + 申诉理由 + + {{ dashouJieshao.length || 0 }}/200 + + + + + + 提示:已从服务器加载最新信息 + + + + + + + + + + + + 加载中... + + + + diff --git a/pages/dashouxiugai/dashouxiugai.wxss b/pages/dashouxiugai/dashouxiugai.wxss new file mode 100644 index 0000000..653e8db --- /dev/null +++ b/pages/dashouxiugai/dashouxiugai.wxss @@ -0,0 +1,197 @@ +/* 页面容器 */ +.container { + min-height: 100vh; + background: linear-gradient(135deg, #fffacd 0%, #e6f7ff 100%); + padding: 20rpx; + box-sizing: border-box; + } + + /* 头像区域 */ + .avatar-section { + display: flex; + justify-content: center; + align-items: center; + padding: 40rpx 0; + position: relative; + } + + .avatar-wrapper { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + } + + .avatar { + width: 180rpx; + height: 180rpx; + border-radius: 50%; + border: 6rpx solid #ffffff; + box-shadow: 0 0 30rpx rgba(255, 215, 0, 0.4); + z-index: 2; + position: relative; + background-color: #f5f5f5; + } + + .avatar-halo { + position: absolute; + width: 220rpx; + height: 220rpx; + border-radius: 50%; + background: radial-gradient(circle, rgba(255, 215, 0, 0.2) 0%, transparent 70%); + z-index: 1; + animation: halo-pulse 2s infinite ease-in-out; + } + + @keyframes halo-pulse { + 0%, 100% { opacity: 0.6; transform: scale(1); } + 50% { opacity: 0.8; transform: scale(1.05); } + } + + .avatar-hint { + margin-top: 20rpx; + font-size: 26rpx; + color: #666; + text-align: center; + } + + /* 信息区域 */ + .info-section { + background: rgba(255, 255, 255, 0.9); + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 40rpx; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.05); + } + + .info-item { + display: flex; + flex-direction: column; + margin-bottom: 40rpx; + position: relative; + padding-bottom: 30rpx; + border-bottom: 1rpx solid #f0f0f0; + } + + .info-item:last-child { + border-bottom: none; + } + + .info-label { + font-size: 28rpx; + color: #333; + font-weight: 600; + margin-bottom: 15rpx; + } + + .info-value-disabled { + font-size: 32rpx; + color: #999; + font-weight: bold; + background: #f9f9f9; + padding: 20rpx; + border-radius: 10rpx; + } + + .info-input { + font-size: 32rpx; + color: #333; + padding: 20rpx; + background: #f9f9f9; + border-radius: 10rpx; + border: 1rpx solid #e0e0e0; + } + + .info-textarea { + font-size: 30rpx; + color: #333; + padding: 20rpx; + background: #f9f9f9; + border-radius: 10rpx; + border: 1rpx solid #e0e0e0; + min-height: 200rpx; + line-height: 1.5; + } + + .info-item-textarea { + padding-bottom: 20rpx; + } + + .char-count { + position: absolute; + right: 0; + bottom: 5rpx; + font-size: 24rpx; + color: #999; + } + + /* 状态提示 */ + .status-section { + background: #e8f4fd; + border-radius: 10rpx; + padding: 20rpx; + margin-bottom: 30rpx; + border-left: 6rpx solid #1890ff; + } + + .status-text { + font-size: 26rpx; + color: #1890ff; + } + + /* 按钮区域 */ + .button-section { + padding: 20rpx 0 40rpx; + } + + .submit-btn { + background: linear-gradient(90deg, #52c41a, #73d13d); + color: white; + font-size: 32rpx; + font-weight: 600; + height: 90rpx; + line-height: 70rpx; + border-radius: 45rpx; + border: none; + box-shadow: 0 10rpx 20rpx rgba(82, 196, 26, 0.2); + } + + .submit-btn:disabled { + background: #cccccc; + box-shadow: none; + } + + .submit-btn::after { + border: none; + } + + /* 加载中 */ + .loading-wrapper { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.9); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + } + + .loading-content { + display: flex; + flex-direction: column; + align-items: center; + } + + .loading-icon { + width: 120rpx; + height: 120rpx; + margin-bottom: 30rpx; + } + + .loading-text { + font-size: 28rpx; + color: #666; + } \ No newline at end of file diff --git a/pages/dingdan/dingdan.js b/pages/dingdan/dingdan.js new file mode 100644 index 0000000..9e31b53 --- /dev/null +++ b/pages/dingdan/dingdan.js @@ -0,0 +1,287 @@ +// pages/dingdan/dingdan.js +const app = getApp() +import request from '../../utils/request.js' + +Page({ + data: { + // 订单状态tab配置 + dingdanZhuangtaiTabs: [ + { name: '全部', key: 'all', zhuangtaiList: [1, 2, 3, 4, 5, 6, 7, 8] }, + { name: '已下单', key: 'yixiadan', zhuangtaiList: [1, 7] }, + { name: '进行中', key: 'jinxingzhong', zhuangtaiList: [2] }, + { name: '争议订单', key: 'zhengyi', zhuangtaiList: [4, 5, 6] }, + { name: '已完成', key: 'yiwancheng', zhuangtaiList: [3] }, + { name: '结算中', key: 'jiesuanzhong', zhuangtaiList: [8] } + ], + + // 当前选中的tab索引 + xuanzhongTabIndex: 0, + // 当前选中的tab的key + currentTabKey: 'all', + + // 每个tab对应的订单数据 + dingdanShuju: { + all: { list: [], page: 1, hasMore: true, isLoading: false }, + yixiadan: { list: [], page: 1, hasMore: true, isLoading: false }, + jinxingzhong: { list: [], page: 1, hasMore: true, isLoading: false }, + zhengyi: { list: [], page: 1, hasMore: true, isLoading: false }, + yiwancheng: { list: [], page: 1, hasMore: true, isLoading: false }, + jiesuanzhong: { list: [], page: 1, hasMore: true, isLoading: false } + }, + + // 个人中心传递过来的type参数 + gerenzhongxinType: '', + + // 是否显示加载更多 + xianshiJiazaigengduo: false, + + // 页面加载状态 + isLoading: true + }, + + onLoad(options) { + const type = options.type || '' + + wx.setNavigationBarTitle({ + title: '我的订单' + }) + + this.qingkongGerenzhongxinJishu(type) + + const tabIndex = this.yingsheTypeToTabIndex(type) + const tabKey = this.data.dingdanZhuangtaiTabs[tabIndex].key + + this.setData({ + gerenzhongxinType: type, + xuanzhongTabIndex: tabIndex, + currentTabKey: tabKey, + isLoading: false + }) + + this.jiazaiDingdanShuju(tabIndex) + }, + onShow() { + // 原有代码... + + // 🆕 注册通知组件 + this.registerNotificationComponent(); +}, + +// 🆕 新增:注册通知组件 +registerNotificationComponent() { + const app = getApp(); + const notificationComp = this.selectComponent('#global-notification'); + + if (notificationComp && notificationComp.showNotification) { + console.log('🏪 商城页面注册通知组件'); + app.globalData.globalNotification = { + show: (data) => notificationComp.showNotification(data), + hide: () => notificationComp.hideNotification() + }; + } +}, + + + qingkongGerenzhongxinJishu(type) { + const jishuMap = { + 'daifuwu': 'daifuwu', + 'fuwuzhong': 'fuwuzhong', + 'yiwancheng': 'yiwancheng', + 'yituikuan': 'yituikuan' + } + + const jishuKey = jishuMap[type] + if (jishuKey && app.globalData.dingdanTiaoshu) { + app.globalData.dingdanTiaoshu[jishuKey] = 0 + } + }, + + yingsheTypeToTabIndex(type) { + const yingsheMap = { + 'daifuwu': 1, + 'fuwuzhong': 2, + 'yiwancheng': 4, + 'yituikuan': 3 + } + + return yingsheMap[type] || 0 + }, + + switchDingdanTab(e) { + const tabIndex = e.currentTarget.dataset.index + if (tabIndex === this.data.xuanzhongTabIndex) return + + const tabKey = this.data.dingdanZhuangtaiTabs[tabIndex].key + + this.setData({ + xuanzhongTabIndex: tabIndex, + currentTabKey: tabKey + }) + + this.jiazaiDingdanShuju(tabIndex) + }, + + async jiazaiDingdanShuju(tabIndex, isLoadMore = false) { + const tab = this.data.dingdanZhuangtaiTabs[tabIndex] + const tabKey = tab.key + + const tabData = this.data.dingdanShuju[tabKey] + + const page = isLoadMore ? tabData.page + 1 : 1 + + if (tabData.isLoading) return + + // 重置逻辑:如果不是加载更多,清空已有数据 + if (!isLoadMore) { + this.setData({ + [`dingdanShuju.${tabKey}.list`]: [], + [`dingdanShuju.${tabKey}.page`]: 1 + }) + } + + this.setData({ + [`dingdanShuju.${tabKey}.isLoading`]: true + }) + + wx.showLoading({ + title: isLoadMore ? '加载更多...' : '加载订单...', + mask: true + }) + + try { + const requestData = { + zhuangtai_list: tab.zhuangtaiList, + page: page, + page_size: 10 + } + + + + const res = await request({ + url: '/dingdan/dingdanhuoqu', + method: 'POST', + data: requestData, + header: { + 'content-type': 'application/json' + } + }) + + + + wx.hideLoading() + + // ✅ 关键修正:res是直接返回的数据对象,不是res.data + if (res && res.data.code === 0) { + // ✅ 关键修正:直接使用res.data.list + const newDingdanList = res.data.data.list || [] + const hasMore = res.data.data.has_more || false + + + + // 处理图片URL,拼接完整路径 + const processedList = newDingdanList.map(item => { + let tupianUrl = item.tupian || '' + + // 如果图片URL不是完整路径,拼接ossImageUrl + if (tupianUrl && !tupianUrl.startsWith('http')) { + const ossImageUrl = app.globalData.ossImageUrl || '' + tupianUrl = ossImageUrl + tupianUrl + } + + const zhuangtaiZh = this.getZhuangtaiZhongwen(item.zhuangtai) + + + return { + ...item, + tupian: tupianUrl, + zhuangtaiZh: zhuangtaiZh + } + }) + + const currentList = this.data.dingdanShuju[tabKey].list + const updatedList = isLoadMore + ? [...currentList, ...processedList] + : processedList + + this.setData({ + [`dingdanShuju.${tabKey}.list`]: updatedList, + [`dingdanShuju.${tabKey}.page`]: page, + [`dingdanShuju.${tabKey}.hasMore`]: hasMore, + [`dingdanShuju.${tabKey}.isLoading`]: false, + xianshiJiazaigengduo: hasMore + }) + + } else { + wx.showToast({ + title: res ? (res.msg || '加载失败') : '请求失败', + icon: 'none', + duration: 2000 + }) + + this.setData({ + [`dingdanShuju.${tabKey}.isLoading`]: false + }) + } + + } catch (error) { + console.error('加载订单数据失败:', error) + wx.hideLoading() + wx.showToast({ + title: '网络请求失败,请重试', + icon: 'none', + duration: 2000 + }) + + this.setData({ + [`dingdanShuju.${tabKey}.isLoading`]: false + }) + } + }, + + getZhuangtaiZhongwen(zhuangtai) { + const zhuangtaiMap = { + 1: '已下单', + 2: '进行中', + 3: '已完成', + 4: '退款中', + 5: '已退款', + 6: '退款失败', + 7: '指定中', + 8: '结算中' + } + + return zhuangtaiMap[zhuangtai] || '未知状态' + }, + + onReachBottom() { + const tabKey = this.data.currentTabKey + const tabData = this.data.dingdanShuju[tabKey] + + if (tabData.hasMore && !tabData.isLoading) { + const tabIndex = this.data.xuanzhongTabIndex + this.jiazaiDingdanShuju(tabIndex, true) + } + }, + + goToDingdanXiangqing(e) { + const dingdanItem = e.currentTarget.dataset.item + if (!dingdanItem) return + + const dingdanDataStr = encodeURIComponent(JSON.stringify(dingdanItem)) + + wx.navigateTo({ + url: `/pages/dingdanxiangqing/dingdanxiangqing?dingdanData=${dingdanDataStr}` + }) + }, + + onImageError(e) { + const index = e.currentTarget.dataset.index + const tabKey = this.data.currentTabKey + + const defaultImage = app.globalData.lunbozhanwei || '/images/default.jpg' + + this.setData({ + [`dingdanShuju.${tabKey}.list[${index}].tupian`]: defaultImage + }) + } +}) \ No newline at end of file diff --git a/pages/dingdan/dingdan.json b/pages/dingdan/dingdan.json new file mode 100644 index 0000000..837f016 --- /dev/null +++ b/pages/dingdan/dingdan.json @@ -0,0 +1,5 @@ +{ + "usingComponents": { + "global-notification": "/components/global-notification/global-notification" + } +} \ No newline at end of file diff --git a/pages/dingdan/dingdan.wxml b/pages/dingdan/dingdan.wxml new file mode 100644 index 0000000..9d64425 --- /dev/null +++ b/pages/dingdan/dingdan.wxml @@ -0,0 +1,111 @@ + + + + + + + + + {{item.name}} + + + + + + + + + + + + + 加载中... + + + + + + 暂无订单 + 快去下单体验服务吧 + + + + + + + + + + + + + + + {{item.jieshao || '暂无描述'}} + + + + + + + + {{item.zhuangtaiZh || '未知状态'}} + + + + + ¥ + {{item.jine || '0.00'}} + + + + + + + + + + 上拉加载更多 + + + + + + 没有更多订单了 + + + + + + diff --git a/pages/dingdan/dingdan.wxss b/pages/dingdan/dingdan.wxss new file mode 100644 index 0000000..afb2316 --- /dev/null +++ b/pages/dingdan/dingdan.wxss @@ -0,0 +1,262 @@ +/* pages/dingdan/dingdan.wxss */ + +.dingdan-page { + min-height: 100vh; + background-color: #f8f9fa; + padding-bottom: 20rpx; + } + + .dingdan-tabs-scroll { + width: 100%; + height: 90rpx; + white-space: nowrap; + background-color: #ffffff; + border-bottom: 1rpx solid #f0f0f0; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.03); + } + + .dingdan-tabs-container { + display: flex; + align-items: center; + height: 100%; + padding: 0 20rpx; + } + + .dingdan-tab-item { + display: inline-flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60rpx; + padding: 0 30rpx; + margin-right: 20rpx; + border-radius: 30rpx; + background-color: #f5f5f5; + color: #666666; + font-size: 28rpx; + transition: all 0.3s ease; + position: relative; + flex-shrink: 0; + } + + .dingdan-tab-item:last-child { + margin-right: 0; + } + + .dingdan-tab-item.tab-active { + background-color: #e8f5e9; + color: #2e7d32; + font-weight: 500; + box-shadow: 0 4rpx 12rpx rgba(46, 125, 50, 0.1); + } + + .tab-active-line { + width: 40rpx; + height: 4rpx; + background-color: #4caf50; + border-radius: 2rpx; + margin-top: 6rpx; + animation: fadeIn 0.3s ease; + } + + @keyframes fadeIn { + from { opacity: 0; transform: scale(0.8); } + to { opacity: 1; transform: scale(1); } + } + + .tab-text { + font-weight: 400; + } + + .dingdan-list-container { + padding: 20rpx; + } + + .loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 400rpx; + } + + .loading-spinner { + width: 60rpx; + height: 60rpx; + border: 6rpx solid #e0e0e0; + border-top-color: #4caf50; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 20rpx; + } + + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + + .loading-text { + font-size: 28rpx; + color: #999999; + } + + .empty-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 500rpx; + padding-top: 80rpx; + } + + .empty-image { + width: 200rpx; + height: 200rpx; + margin-bottom: 30rpx; + opacity: 0.6; + } + + .empty-text { + font-size: 32rpx; + color: #999999; + margin-bottom: 10rpx; + } + + .empty-subtext { + font-size: 26rpx; + color: #cccccc; + } + + .dingdan-card { + display: flex; + align-items: center; + background-color: #ffffff; + border-radius: 16rpx; + padding: 24rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); + transition: transform 0.2s ease, box-shadow 0.2s ease; + } + + .dingdan-card:active { + transform: translateY(-2rpx); + box-shadow: 0 6rpx 24rpx rgba(0, 0, 0, 0.08); + } + + .dingdan-card-left { + flex-shrink: 0; + margin-right: 24rpx; + } + + .dingdan-image { + width: 160rpx; + height: 160rpx; + border-radius: 12rpx; + background-color: #f5f5f5; + } + + .dingdan-card-center { + flex: 1; + height: 160rpx; + display: flex; + align-items: flex-start; + } + + .dingdan-jieshao { + font-size: 28rpx; + line-height: 40rpx; + color: #333333; + font-weight: 400; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + text-overflow: ellipsis; + } + + .dingdan-card-right { + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: space-between; + height: 160rpx; + margin-left: 20rpx; + } + + .dingdan-zhuangtai { + font-size: 24rpx; + color: #ff6b6b; + font-weight: 500; + padding: 6rpx 16rpx; + border-radius: 20rpx; + background-color: #fff5f5; + white-space: nowrap; + } + + /* ✅ 修正后的价格样式 - 简单明了 */ + .dingdan-jine { + display: flex; + align-items: center; /* 垂直居中 */ + } + + .jine-symbol { + font-size: 28rpx; + color: #333333; + font-weight: 500; + margin-right: 4rpx; /* 添加右边距,让符号和数字有间隔 */ + } + + .jine-number { + font-size: 32rpx; /* 整数和小数一样大 */ + color: #333333; + font-weight: 600; + } + + .load-more-container { + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx 0; + } + + .load-more-line { + width: 80rpx; + height: 1rpx; + background-color: #e0e0e0; + margin: 0 20rpx; + } + + .load-more-text { + font-size: 26rpx; + color: #999999; + white-space: nowrap; + } + + .no-more-container { + text-align: center; + padding: 40rpx 0; + } + + .no-more-text { + font-size: 26rpx; + color: #cccccc; + } + + @media (max-width: 375px) { + .dingdan-image { + width: 140rpx; + height: 140rpx; + } + + .dingdan-card-center { + height: 140rpx; + } + + .dingdan-card-right { + height: 140rpx; + } + + .jine-number { + font-size: 30rpx; + } + } \ No newline at end of file diff --git a/pages/dingdanxiangqing/dingdanxiangqing.js b/pages/dingdanxiangqing/dingdanxiangqing.js new file mode 100644 index 0000000..9e5fa60 --- /dev/null +++ b/pages/dingdanxiangqing/dingdanxiangqing.js @@ -0,0 +1,412 @@ +// pages/dingdanxiangqing/dingdanxiangqing.js +const app = getApp() +import request from '../../utils/request.js' + +Page({ + data: { + // 从跳转参数获取的基础数据 + jibenShuju: { + dingdanId: '', + tupian: '', + jieshao: '', + chuangjianshijian: '', + jine: '', + nicheng: '', + beizhu: '', + zhuangtai: 0 + }, + + // 从后端获取的详细数据 + xiangxiShuju: { + zhiding: '', // 指定打手 + dashou: '', // 服务打手 + dashoujiaofu: [], // 打手交付图片列表 + dashouliuyan: '' // 打手留言 + }, + + // 结算相关数据 + jiesuanShuju: { + pingfen: 0, // 评分 0-5 + liuyan: '' // 留言 + }, + + // 页面状态 + isLoading: true, // 页面加载中 + isJiesuanLoading: false, // 结单请求中 + + // 状态颜色映射 + zhuangtaiYanseMap: { + 1: '#E8F5E9', // 已下单 - 浅绿色 + 2: '#E3F2FD', // 进行中 - 浅蓝色 + 3: '#F3E5F5', // 已完成 - 浅紫色 + 4: '#FFF3E0', // 退款中 - 浅橙色 + 5: '#FFEBEE', // 已退款 - 浅红色 + 6: '#EFEBE9', // 退款失败 - 浅棕色 + 7: '#E0F7FA', // 指定中 - 浅青色 + 8: '#FFF8E1' // 结算中 - 浅黄色 + }, + + // 状态文字映射 + zhuangtaiWenziMap: { + 1: '已下单', + 2: '进行中', + 3: '已完成', + 4: '退款中', + 5: '已退款', + 6: '退款失败', + 7: '指定中', + 8: '结算中' + }, + + // 星星评分数据 + xingxingData: [ + { id: 1, active: false }, + { id: 2, active: false }, + { id: 3, active: false }, + { id: 4, active: false }, + { id: 5, active: false } + ] + }, + + onLoad(options) { + // 设置页面标题 + wx.setNavigationBarTitle({ + title: '订单详情' + }) + + // 解析跳转参数 + this.jiexiTiaozhuanCanshu(options) + }, + onShow() { + // 原有代码... + + // 🆕 注册通知组件 + this.registerNotificationComponent(); +}, + +// 🆕 新增:注册通知组件 +registerNotificationComponent() { + const app = getApp(); + const notificationComp = this.selectComponent('#global-notification'); + + if (notificationComp && notificationComp.showNotification) { + console.log('🏪 商城页面注册通知组件'); + app.globalData.globalNotification = { + show: (data) => notificationComp.showNotification(data), + hide: () => notificationComp.hideNotification() + }; + } +}, + + /** + * 解析跳转参数 + */ + jiexiTiaozhuanCanshu(options) { + try { + const dingdanDataStr = options.dingdanData || '' + if (!dingdanDataStr) { + wx.showToast({ + title: '订单数据错误', + icon: 'none' + }) + setTimeout(() => { + wx.navigateBack() + }, 1500) + return + } + + // 解析订单数据 + const dingdanData = JSON.parse(decodeURIComponent(dingdanDataStr)) + + + + // 处理图片URL(拼接完整路径) + let tupianUrl = dingdanData.tupian || '' + if (tupianUrl && !tupianUrl.startsWith('http')) { + const ossImageUrl = app.globalData.ossImageUrl || '' + tupianUrl = ossImageUrl + tupianUrl + } + + // 格式化价格 + let jine = dingdanData.jine || 0 + jine = parseFloat(jine) + if (isNaN(jine)) { + jine = 0 + } + const formattedJine = jine.toFixed(2) + + // 设置基础数据(状态先用跳转的,等后端返回后更新) + this.setData({ + 'jibenShuju.dingdanId': dingdanData.dingdanId || '', + 'jibenShuju.tupian': tupianUrl, + 'jibenShuju.jieshao': dingdanData.jieshao || '', + 'jibenShuju.chuangjianshijian': dingdanData.chuangjianshijian || '', + 'jibenShuju.jine': formattedJine, + 'jibenShuju.nicheng': dingdanData.nicheng || '', + 'jibenShuju.beizhu': dingdanData.beizhu || '', + // 这里先使用跳转的状态,等后端返回后会被覆盖 + 'jibenShuju.zhuangtai': dingdanData.zhuangtai || 0, + isLoading: false + }) + + + + // 加载详细数据 + this.jiazaiXiangxiShuju(dingdanData.dingdanId) + + } catch (error) { + console.error('解析订单数据失败:', error) + wx.showToast({ + title: '数据解析失败', + icon: 'none' + }) + setTimeout(() => { + wx.navigateBack() + }, 1500) + } + }, + + /** + * 加载详细数据 + */ + async jiazaiXiangxiShuju(dingdanId) { + if (!dingdanId) return + + + + try { + const res = await request({ + url: '/dingdan/dingdanxiangqing', + method: 'POST', + data: { dingdanId: dingdanId }, + header: { 'content-type': 'application/json' } + }) + + + + // 关键修正:使用正确的数据访问路径 + if (res && res.data.code === 0 && res.data.data) { + const data = res.data.data + + + + // 处理打手交付图片(拼接完整URL) + let dashoujiaofuList = data.dashoujiaofu || [] + dashoujiaofuList = dashoujiaofuList.map(imgUrl => { + if (imgUrl && !imgUrl.startsWith('http')) { + const ossImageUrl = app.globalData.ossImageUrl || '' + return ossImageUrl + imgUrl + } + return imgUrl + }) + + // 核心修正:使用后端返回的订单状态,覆盖跳转时的状态 + this.setData({ + 'xiangxiShuju.zhiding': data.zhiding || '', + 'xiangxiShuju.dashou': data.dashou || '', + 'xiangxiShuju.dashoujiaofu': dashoujiaofuList, + 'xiangxiShuju.dashouliuyan': data.dashouliuyan || '', + // 关键:使用后端返回的状态覆盖跳转的状态 + 'jibenShuju.zhuangtai': data.zhuangtai || 0 + }) + + + + } else { + console.warn('获取详细数据失败:', res?.msg || '未知错误') + wx.showToast({ + title: res?.msg || '获取订单详情失败', + icon: 'none', + duration: 2000 + }) + } + + } catch (error) { + console.error('加载详细数据失败:', error) + wx.showToast({ + title: '加载数据失败,请重试', + icon: 'none', + duration: 2000 + }) + } + }, + + /** + * 复制文本到剪贴板 + */ + async fuzhiWenben(e) { + const text = e.currentTarget.dataset.text + if (!text) return + + + + try { + await wx.setClipboardData({ + data: text + }) + + wx.showToast({ + title: '复制成功', + icon: 'success', + duration: 1500 + }) + + } catch (error) { + console.error('复制失败:', error) + wx.showToast({ + title: '复制失败', + icon: 'none' + }) + } + }, + + /** + * 预览图片 + */ + yulanTupian(e) { + const currentUrl = e.currentTarget.dataset.url + const urls = e.currentTarget.dataset.urls || [] + + if (!currentUrl) return + + wx.previewImage({ + current: currentUrl, + urls: urls.length > 0 ? urls : [currentUrl] + }) + }, + + /** + * 点击星星评分 + */ + dianjiXingxing(e) { + const pingfen = e.currentTarget.dataset.index + + // 更新星星状态 + const xingxingData = this.data.xingxingData.map((item, index) => ({ + ...item, + active: index < pingfen + })) + + this.setData({ + xingxingData: xingxingData, + 'jiesuanShuju.pingfen': pingfen + }) + + + }, + + /** + * 输入留言 + */ + shuruLiuyan(e) { + const liuyan = e.detail.value.slice(0, 50) // 限制50字 + this.setData({ + 'jiesuanShuju.liuyan': liuyan + }) + }, + + /** + * 提交结单 + */ + async tijiaoJiedan() { + const { jibenShuju, jiesuanShuju } = this.data + + console.log('提交结单,数据:', { + dingdanId: jibenShuju.dingdanId, + pingfen: jiesuanShuju.pingfen, + liuyan: jiesuanShuju.liuyan + }) + + // 验证订单状态 + if (jibenShuju.zhuangtai !== 8) { + wx.showToast({ + title: '订单不在结算状态', + icon: 'none' + }) + return + } + + // 验证评分范围 + if (jiesuanShuju.pingfen < 0 || jiesuanShuju.pingfen > 5) { + wx.showToast({ + title: '请选择0-5分评分', + icon: 'none' + }) + return + } + + // 验证订单ID + if (!jibenShuju.dingdanId) { + wx.showToast({ + title: '订单信息错误', + icon: 'none' + }) + return + } + + // 显示加载中 + this.setData({ isJiesuanLoading: true }) + wx.showLoading({ + title: '提交中...', + mask: true + }) + + try { + const res = await request({ + url: '/dingdan/jiedan', + method: 'POST', + data: { + dingdanId: jibenShuju.dingdanId, + pingfen: jiesuanShuju.pingfen, + liuyan: jiesuanShuju.liuyan + }, + header: { 'content-type': 'application/json' } + }) + + + + wx.hideLoading() + this.setData({ isJiesuanLoading: false }) + + // 使用正确的数据访问路径 + if (res && res.data.code === 0) { + // 结单成功,更新订单状态 + this.setData({ + 'jibenShuju.zhuangtai': 3, // 变为已完成状态 + 'jiesuanShuju.pingfen': 0, + 'jiesuanShuju.liuyan': '' + }) + + // 重置星星 + const resetXingxingData = this.data.xingxingData.map(item => ({ + ...item, + active: false + })) + this.setData({ xingxingData: resetXingxingData }) + + wx.showToast({ + title: '结单成功', + icon: 'success', + duration: 2000 + }) + + } else { + wx.showToast({ + title: res?.msg || '结单失败', + icon: 'none' + }) + } + + } catch (error) { + console.error('结单失败:', error) + wx.hideLoading() + this.setData({ isJiesuanLoading: false }) + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }) + } + }, + + +}) \ No newline at end of file diff --git a/pages/dingdanxiangqing/dingdanxiangqing.json b/pages/dingdanxiangqing/dingdanxiangqing.json new file mode 100644 index 0000000..837f016 --- /dev/null +++ b/pages/dingdanxiangqing/dingdanxiangqing.json @@ -0,0 +1,5 @@ +{ + "usingComponents": { + "global-notification": "/components/global-notification/global-notification" + } +} \ No newline at end of file diff --git a/pages/dingdanxiangqing/dingdanxiangqing.wxml b/pages/dingdanxiangqing/dingdanxiangqing.wxml new file mode 100644 index 0000000..d0a1ab8 --- /dev/null +++ b/pages/dingdanxiangqing/dingdanxiangqing.wxml @@ -0,0 +1,202 @@ + + + + + + + 加载订单详情... + + + + + + + + + + + + {{jibenShuju.jieshao || '暂无描述'}} + + + + + + 订单信息 + + + + 游戏昵称: + {{jibenShuju.nicheng}} + + + + + 备注信息: + {{jibenShuju.beizhu}} + + + + + 指定打手: + {{xiangxiShuju.zhiding}} + + + + + 订单ID: + + {{jibenShuju.dingdanId}} + + 📋 + 复制 + + + + + + + 创建时间: + {{jibenShuju.chuangjianshijian}} + + + + + 订单价格: + ¥{{jibenShuju.jine}} + + + + + 订单状态: + {{zhuangtaiWenziMap[jibenShuju.zhuangtai] || '未知状态'}} + + + + + + 服务详情 + + + + 服务打手: + + {{xiangxiShuju.dashou}} + + 📋 + 复制 + + + + + + + 打手交付 + + + + + + + + + + + + 打手留言 + + {{xiangxiShuju.dashouliuyan}} + + + + + + + 订单结算 + + + + + 打星评分: + + + + {{item.active ? '★' : '☆'}} + + + + + + + + + 订单留言(可选) + + + 已输入 {{jiesuanShuju.liuyan.length}}/50 字 + + + + + + + + + + + + + + diff --git a/pages/dingdanxiangqing/dingdanxiangqing.wxss b/pages/dingdanxiangqing/dingdanxiangqing.wxss new file mode 100644 index 0000000..686128a --- /dev/null +++ b/pages/dingdanxiangqing/dingdanxiangqing.wxss @@ -0,0 +1,422 @@ +/* pages/dingdanxiangqing/dingdanxiangqing.wxss */ + +/* 页面基础样式 */ +.dingdanxiangqing-page { + min-height: 100vh; + background-color: #f8f9fa; + padding-bottom: 140rpx; + } + + /* 加载中状态 */ + .loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; + } + + .loading-spinner { + width: 80rpx; + height: 80rpx; + border: 8rpx solid #e0e0e0; + border-top-color: #4caf50; + border-radius: 50%; + animation: spin 1.2s linear infinite; + margin-bottom: 30rpx; + } + + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + + .loading-text { + font-size: 30rpx; + color: #666666; + } + + /* 内容区域 */ + .content-container { + padding: 25rpx; + } + + /* 商品图片和介绍区域 */ + .shangpin-area { + display: flex; + background-color: #ffffff; + border-radius: 20rpx; + padding: 25rpx; + margin-bottom: 25rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06); + } + + .shangpin-image-container { + flex-shrink: 0; + width: 180rpx; + height: 180rpx; + margin-right: 25rpx; + } + + .shangpin-image { + width: 100%; + height: 100%; + border-radius: 12rpx; + background-color: #f5f5f5; + } + + .shangpin-jieshao { + flex: 1; + display: flex; + align-items: flex-start; + } + + .jieshao-text { + font-size: 28rpx; + line-height: 42rpx; + color: #333333; + font-weight: 400; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + overflow: hidden; + text-overflow: ellipsis; + } + + /* 通用区域样式 */ + .info-title { + font-size: 32rpx; + color: #333333; + font-weight: 600; + margin-bottom: 20rpx; + padding-bottom: 12rpx; + border-bottom: 2rpx solid rgba(0, 0, 0, 0.08); + } + + .section-subtitle { + font-size: 28rpx; + color: #555555; + font-weight: 500; + display: block; + } + + /* 左右布局的订单信息项 */ + .dingdan-info-area { + background-color: #ffffff; + border-radius: 20rpx; + padding: 25rpx; + margin-bottom: 25rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06); + } + + .info-item-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 18rpx; + padding: 12rpx 0; + } + + .info-item-row:last-child { + margin-bottom: 0; + } + + .item-label { + font-size: 28rpx; + color: #666666; + font-weight: 400; + flex-shrink: 0; + min-width: 150rpx; + } + + /* 右侧区域容器 */ + .item-right-area { + flex: 1; + display: flex; + align-items: center; + justify-content: flex-end; + } + + .item-value { + font-size: 28rpx; + color: #333333; + font-weight: 500; + word-break: break-all; + text-align: right; + margin-right: 15rpx; + } + + /* 特殊样式 */ + .jine-red { + color: #ff4444 !important; + font-weight: 600 !important; + font-size: 32rpx !important; + } + + .zhuangtai-text { + font-weight: 600 !important; + font-size: 30rpx !important; + color: #333333 !important; + } + + /* 复制按钮 */ + .copy-btn { + display: flex; + align-items: center; + justify-content: center; + font-size: 24rpx; + color: #4caf50; + background-color: #e8f5e9; + padding: 6rpx 16rpx; + border-radius: 20rpx; + border: 1rpx solid #4caf50; + transition: all 0.2s ease; + white-space: nowrap; + flex-shrink: 0; + } + + .copy-btn-hover { + background-color: #c8e6c9; + transform: scale(0.95); + } + + .copy-icon { + font-size: 20rpx; + margin-right: 6rpx; + } + + .copy-text { + font-weight: 500; + } + + /* 服务区域 */ + .fuwu-area { + background-color: #ffffff; + border-radius: 20rpx; + padding: 25rpx; + margin-bottom: 25rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06); + } + + /* 打手交付图片网格 */ + .dashou-jiaofu-container { + margin-bottom: 20rpx; + margin-top: 15rpx; + } + + .jiaofu-grid { + display: flex; + flex-wrap: wrap; + margin: -5rpx; + } + + .jiaofu-image-item { + width: calc(33.333% - 10rpx); + margin: 5rpx; + aspect-ratio: 1/1; + } + + .jiaofu-image { + width: 100%; + height: 100%; + border-radius: 12rpx; + background-color: #f5f5f5; + transition: transform 0.2s ease; + } + + .jiaofu-image:active { + transform: scale(0.95); + } + + /* 打手留言 */ + .dashou-liuyan-container { + margin-top: 15rpx; + } + + .liuyan-content { + font-size: 26rpx; + line-height: 40rpx; + color: #555555; + padding: 15rpx; + background-color: #f9f9f9; + border-radius: 12rpx; + border-left: 4rpx solid #4caf50; + } + + /* 结算区域 */ + .jiesuan-area { + background-color: #ffffff; + border-radius: 20rpx; + padding: 25rpx; + margin-bottom: 25rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06); + } + + /* 星星评分布局修正 */ + .pingfen-container { + margin-bottom: 25rpx; + } + + .xingxing-layout { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 10rpx; + } + + .xingxing-wrapper { + display: flex; + align-items: center; + } + + .xingxing-icon { + font-size: 52rpx; + color: #cccccc; + margin-right: 10rpx; + transition: all 0.3s ease; + cursor: pointer; + /* 去掉点击时的蓝色矩形框 */ + user-select: none; + -webkit-tap-highlight-color: transparent; + outline: none; + border: none; + background: none; + } + + .xingxing-icon:last-child { + margin-right: 0; + } + + .xingxing-active { + color: #FFD700; + text-shadow: 0 0 10rpx rgba(255, 215, 0, 0.5); + transform: scale(1.1); + } + + .xingxing-icon:active { + transform: scale(0.9); + } + + /* 留言输入 */ + .liuyan-input-container { + margin-top: 15rpx; + } + + .liuyan-textarea { + width: 100%; + min-height: 120rpx; + font-size: 28rpx; + color: #333333; + background-color: #f9f9f9; + border-radius: 12rpx; + padding: 20rpx; + margin-top: 10rpx; + box-sizing: border-box; + } + + .zi-shu { + display: block; + text-align: right; + font-size: 24rpx; + color: #999999; + margin-top: 8rpx; + } + + /* 底部安全区域 */ + .safe-area { + height: 40rpx; + } + + /* 结单按钮容器修正 */ + .jiedan-btn-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(to top, rgba(255,255,255,0.9), transparent); + padding: 20rpx 30rpx; + backdrop-filter: blur(10rpx); + z-index: 100; + } + + .jiedan-btn { + background: linear-gradient(135deg, #4caf50, #2e7d32); + color: #ffffff; + border: none; + border-radius: 50rpx; + height: 90rpx; + font-size: 32rpx; + font-weight: 600; + position: relative; + overflow: hidden; + box-shadow: 0 6rpx 20rpx rgba(76, 175, 80, 0.3); + transition: all 0.3s ease; + width: 100%; + /* 垂直居中 */ + display: flex; + align-items: center; + justify-content: center; + } + + .jiedan-btn::after { + border: none; + } + + .btn-hover { + transform: translateY(-2rpx); + box-shadow: 0 8rpx 25rpx rgba(76, 175, 80, 0.4); + } + + .btn-text { + position: relative; + z-index: 2; + /* 确保文字垂直居中 */ + line-height: 1; + } + + /* 按钮闪光特效 */ + .btn-sparkle { + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(255,255,255,0.3) 1%, transparent 10%); + animation: sparkle 2s infinite linear; + z-index: 1; + } + + @keyframes sparkle { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + + /* 响应式调整 */ + @media (max-width: 375px) { + .shangpin-image-container { + width: 160rpx; + height: 160rpx; + } + + .xingxing-icon { + font-size: 48rpx; + } + + .jiedan-btn { + height: 85rpx; + font-size: 30rpx; + } + + .item-label { + min-width: 140rpx; + font-size: 26rpx; + } + + .item-value { + font-size: 26rpx; + } + + .jiedan-btn-container { + padding: 20rpx 25rpx; + } + } \ No newline at end of file diff --git a/pages/dsddxq/dsddxq.js b/pages/dsddxq/dsddxq.js new file mode 100644 index 0000000..e39e235 --- /dev/null +++ b/pages/dsddxq/dsddxq.js @@ -0,0 +1,721 @@ +// pages/dsddxq/dsddxq.js - 【最终版:提交前公告弹窗 + 修改图片至少保留一张】 +const app = getApp() +import request from '../../utils/request.js' +const COS = require('../../utils/cos-wx-sdk-v5.js') +import connectionManager from '../../utils/xiaoxilj.js' + +Page({ + data: { + jibenShuju: { + dingdan_id: '', + tupian: '', + jieshao: '', + create_time: '', + jine: '', + nicheng: '', + beizhu: '', + zhuangtai: 0, + fadanpingtai: 0 + }, + xiangxiShuju: { + shangjia_id: '', + shangjia_nicheng: '', + shangjia_liuyan: '', + tuikuan_liyou: '', + chufa_liyou: '', + chufa_zhuangtai: 0, + chufa_jieguo: '', + bohui_liyou: '', + dashou_jiaofu: [], + dashou_liuyan: '', + laoban_id: '' + }, + zhuangtaiYanseMap: { + 1: '#E8F5E9', 2: '#E3F2FD', 3: '#F3E5F5', 4: '#FFF3E0', + 5: '#FFEBEE', 6: '#EFEBE9', 7: '#E0F7FA', 8: '#FFF8E1' + }, + zhuangtaiWenziMap: { + 1: '已下单', 2: '进行中', 3: '已完成', 4: '退款中', + 5: '已退款', 6: '退款失败', 7: '指定中', 8: '结算中' + }, + isLoading: true, + isSubmitting: false, + liuyan: '', + xuanzhongTupian: [], + shangchuanJindu: 0, + shangchuanZongshu: 0, + jinduWidth: '0%', + showWanzhengJieshao: false, + wanzhengJieshao: '', + showWanzhengBeizhu: false, + wanzhengBeizhu: '', + ossImageUrl: '', + morentouxiang: '', + showRefundInfo: false, + + // 修改功能所需字段 + isModifying: false, + originalDashouJiaofu: [], + originalDashouLiuyan: '', + remainingOriginals: [], + modifiedLiuyan: '', + deletedImages: [], + newImages: [], + isConfirming: false, + modifyUploadProgress: { total: 0, current: 0, width: '0%' } + }, + + onLoad(options) { + wx.setNavigationBarTitle({ title: '订单详情' }) + this.initGlobalData() + this.jiexiTiaozhuanCanshu(options) + }, + onHide() { + const popupComp = this.selectComponent('#popupNotice'); + if (popupComp && popupComp.cleanup) { + popupComp.cleanup(); + } + }, + + onShow() { + this.registerNotificationComponent(); + }, + + registerNotificationComponent() { + const app = getApp(); + const notificationComp = this.selectComponent('#global-notification'); + if (notificationComp && notificationComp.showNotification) { + app.globalData.globalNotification = { + show: (data) => notificationComp.showNotification(data), + hide: () => notificationComp.hideNotification() + }; + } + }, + + initGlobalData() { + const app = getApp() + const ossImageUrl = app.globalData.ossImageUrl || 'https://your-oss-domain.com/' + const morentouxiang = app.globalData.morentouxiang || '/images/default-avatar.png' + this.setData({ ossImageUrl, morentouxiang }) + }, + + jiexiTiaozhuanCanshu(options) { + try { + const dingdanDataStr = options.dingdanData || '' + if (!dingdanDataStr) { + wx.showToast({ title: '订单数据错误', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + return + } + const dingdanData = JSON.parse(decodeURIComponent(dingdanDataStr)) + let tupianUrl = dingdanData.tupian || '' + if (!tupianUrl) tupianUrl = this.getTupianUrl() + let jine = parseFloat(dingdanData.jine || 0) + if (isNaN(jine)) jine = 0 + const formattedJine = jine.toFixed(2) + this.setData({ + 'jibenShuju.dingdan_id': dingdanData.dingdan_id || '', + 'jibenShuju.tupian': tupianUrl, + 'jibenShuju.jieshao': dingdanData.jieshao || '', + 'jibenShuju.create_time': dingdanData.create_time || '', + 'jibenShuju.jine': formattedJine, + 'jibenShuju.nicheng': dingdanData.nicheng || '', + 'jibenShuju.beizhu': dingdanData.beizhu || '', + 'jibenShuju.zhuangtai': dingdanData.zhuangtai || 0, + 'jibenShuju.fadanpingtai': dingdanData.fadanpingtai || 0, + isLoading: false + }) + this.jiazaiXiangxiShuju(dingdanData.dingdan_id) + } catch (error) { + console.error('解析订单数据失败:', error) + wx.showToast({ title: '数据解析失败', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + } + }, + + getTupianUrl() { + const cacheTouxiang = wx.getStorageSync('touxiang') || '' + let tupianPath = cacheTouxiang || app.globalData.morentouxiang || '' + const ossImageUrl = app.globalData.ossImageUrl || '' + if (tupianPath && ossImageUrl) { + if (tupianPath.startsWith('/')) tupianPath = tupianPath.substring(1) + return ossImageUrl + tupianPath + } + return '' + }, + + async jiazaiXiangxiShuju(dingdanId) { + if (!dingdanId) return + wx.showLoading({ title: '加载中...', mask: true }) + try { + const res = await request({ + url: '/dingdan/dsddxq', + method: 'POST', + data: { dingdan_id: dingdanId }, + header: { 'content-type': 'application/json' } + }) + wx.hideLoading() + if (res && res.data.code === 0 && res.data.data) { + const data = res.data.data + let dashouJiaofuList = data.dashou_jiaofu || [] + dashouJiaofuList = dashouJiaofuList.map(imgUrl => { + if (imgUrl && !imgUrl.startsWith('http')) { + const ossImageUrl = app.globalData.ossImageUrl || '' + if (imgUrl.startsWith('/')) imgUrl = imgUrl.substring(1) + return ossImageUrl + imgUrl + } + return imgUrl + }) + this.setData({ + 'xiangxiShuju.shangjia_id': data.shangjia_id || '', + 'xiangxiShuju.shangjia_nicheng': data.shangjia_nicheng || '', + 'xiangxiShuju.shangjia_liuyan': data.shangjia_liuyan || '', + 'xiangxiShuju.tuikuan_liyou': data.tuikuan_liyou || '', + 'xiangxiShuju.chufa_liyou': data.chufa_liyou || '', + 'xiangxiShuju.chufa_zhuangtai': data.chufa_zhuangtai || 0, + 'xiangxiShuju.chufa_jieguo': data.chufa_jieguo || '', + 'xiangxiShuju.bohui_liyou': data.bohui_liyou || '', + 'xiangxiShuju.dashou_jiaofu': dashouJiaofuList, + 'xiangxiShuju.dashou_liuyan': data.dashou_liuyan || '', + 'xiangxiShuju.laoban_id': data.laoban_id || '', + 'jibenShuju.zhuangtai': data.zhuangtai || 0, + 'jibenShuju.beizhu': data.beizhu || '', + 'liuyan': data.dashou_liuyan || '', + 'jibenShuju.fadanpingtai': data.fadanpingtai || 0 + }) + const currentStatus = data.zhuangtai || 0 + const showRefund = [4,5,6].includes(currentStatus) + this.setData({ showRefundInfo: showRefund }) + } else { + const errorMsg = res?.data?.msg || '获取订单详情失败' + wx.showToast({ title: errorMsg, icon: 'none', duration: 2000 }) + } + } catch (error) { + console.error('加载详细数据失败:', error) + wx.hideLoading() + wx.showToast({ title: '加载数据失败,请重试', icon: 'none', duration: 2000 }) + } + }, + + // ========== 原有提交功能(部分修改) ========== + xuanzeTupian() { + if (this.data.isSubmitting) return + const shengyu = 9 - this.data.xuanzhongTupian.length + if (shengyu <= 0) { + wx.showToast({ title: '最多只能选择9张图片', icon: 'none' }) + return + } + wx.chooseImage({ + count: shengyu, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: (res) => { + const newTupian = [...this.data.xuanzhongTupian, ...res.tempFilePaths] + this.setData({ xuanzhongTupian: newTupian.slice(0, 9) }) + } + }) + }, + shanchuTupian(e) { + const index = e.currentTarget.dataset.index + const newTupian = [...this.data.xuanzhongTupian] + newTupian.splice(index, 1) + this.setData({ xuanzhongTupian: newTupian }) + }, + yulanTupian(e) { + const currentUrl = e.currentTarget.dataset.url + const urls = e.currentTarget.dataset.urls || [] + if (!currentUrl) return + wx.previewImage({ current: currentUrl, urls: urls.length > 0 ? urls : [currentUrl] }) + }, + getFileExtension(filePath) { + const lastDotIndex = filePath.lastIndexOf('.') + if (lastDotIndex === -1) return '.jpg' + const ext = filePath.substring(lastDotIndex).toLowerCase() + const allowedExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'] + return allowedExts.includes(ext) ? ext : '.jpg' + }, + shuruLiuyan(e) { + const liuyan = e.detail.value.slice(0, 50) + this.setData({ liuyan }) + }, + chakanWanzhengJieshao() { + this.setData({ showWanzhengJieshao: true, wanzhengJieshao: this.data.jibenShuju.jieshao || '无' }) + }, + guanbiWanzhengJieshao() { this.setData({ showWanzhengJieshao: false }) }, + chakanWanzhengBeizhu() { + this.setData({ showWanzhengBeizhu: true, wanzhengBeizhu: this.data.jibenShuju.beizhu || '无' }) + }, + guanbiWanzhengBeizhu() { this.setData({ showWanzhengBeizhu: false }) }, + async fuzhiWenben(e) { + const text = e.currentTarget.dataset.text + if (!text) return + try { + await wx.setClipboardData({ data: text }) + wx.showToast({ title: '复制成功', icon: 'success', duration: 1500 }) + } catch (error) { + console.error('复制失败:', error) + wx.showToast({ title: '复制失败', icon: 'none' }) + } + }, + goToKefu() { + const kefuLink = app.globalData.kefuConfig?.link || ''; + const corpId = app.globalData.kefuConfig?.enterpriseId || ''; + + if (!kefuLink || !corpId) { + wx.showToast({ + title: '客服配置未加载', + icon: 'none' + }); + return; + } + + wx.openCustomerServiceChat({ + extInfo: { url: kefuLink }, + corpId: corpId, + success: (res) => { + console.log('跳转客服成功', res); + }, + fail: (err) => { + console.error('跳转客服失败', err); + wx.showToast({ + title: '暂时无法连接客服', + icon: 'none' + }); + } + }); + }, + + // ===== 联系老板按钮(改为跳转订单群聊) ===== + // ===== 联系老板按钮(智能查找订单群聊) ===== +// ===== 联系老板按钮(智能查找订单群聊) ===== +goToChatWithBoss() { + const uid = wx.getStorageSync('uid') + if (!uid) return + + const orderId = this.data.jibenShuju.dingdan_id + if (!orderId) { + wx.showToast({ title: '订单ID缺失', icon: 'none' }) + return + } + + const identityType = 'dashou' + const userId = 'Ds' + uid + + connectionManager.connectToGroupChat({ + identityType, + userId, + orderId, + groupName: this.data.jibenShuju.nicheng + '的订单群', + isCross: this.data.jibenShuju.fadanpingtai || 0 + }).catch(err => { + console.error('跳转群聊失败', err) + }) + }, + + async getCOSZhengshu() { + try { + const res = await request({ + url: '/dingdan/dsscpz', + method: 'POST', + data: { dingdan_id: this.data.jibenShuju.dingdan_id } + }) + if (res && res.data.code === 0 && res.data.data) return res.data.data + else throw new Error(res?.data?.msg || '获取上传凭证失败') + } catch (error) { + console.error('获取COS凭证失败:', error) + throw error + } + }, + + initCOSClient(tokenData) { + const credentials = tokenData.credentials || tokenData + const bucket = tokenData.bucket || 'julebu-1361527063' + const region = tokenData.region || 'ap-shanghai' + const cos = new COS({ + SimpleUploadMethod: 'putObject', + getAuthorization: async (options, callback) => { + const authParams = { + TmpSecretId: credentials.tmpSecretId, + TmpSecretKey: credentials.tmpSecretKey, + SecurityToken: credentials.sessionToken || '', + StartTime: tokenData.startTime || Math.floor(Date.now() / 1000), + ExpiredTime: tokenData.expiredTime || (Math.floor(Date.now() / 1000) + 7200) + } + callback(authParams) + } + }) + return { cos, bucket, region } + }, + + async shangchuanDanZhangTupian(filePath, cosKey, index, cosInstance, bucket, region) { + return new Promise((resolve) => { + const timeoutId = setTimeout(() => resolve({ status: 'optimistic_success', key: cosKey }), 2000) + cosInstance.uploadFile({ + Bucket: bucket, + Region: region, + Key: cosKey, + FilePath: filePath, + onProgress: (progressInfo) => { + const percent = Math.round(progressInfo.percent * 100) + if (percent === 100) { + clearTimeout(timeoutId) + resolve({ status: 'optimistic_success', key: cosKey }) + } + } + }) + }) + }, + + async piliangShangchuanTupian() { + const total = this.data.xuanzhongTupian.length + this.setData({ shangchuanZongshu: total, shangchuanJindu: 0, jinduWidth: '0%' }) + wx.showLoading({ title: '正在准备...', mask: true }) + const preGeneratedUrls = [] + for (let i = 0; i < total; i++) { + const timestamp = Date.now() + i + const random = Math.floor(Math.random() * 10000) + const fileExt = this.getFileExtension(this.data.xuanzhongTupian[i]) + const fileName = `${timestamp}_${random}${fileExt}` + const cosKey = `order/${this.data.jibenShuju.dingdan_id}/${fileName}` + preGeneratedUrls.push(cosKey) + } + let cosClient, bucket, region + try { + const tokenData = await this.getCOSZhengshu() + const { cos, bucket: cosBucket, region: cosRegion } = this.initCOSClient(tokenData) + cosClient = cos + bucket = cosBucket + region = cosRegion + } catch (error) { + wx.hideLoading() + wx.showToast({ title: '上传初始化失败', icon: 'none' }) + return [] + } + const uploadTasks = [] + for (let i = 0; i < total; i++) { + const task = this.shangchuanDanZhangTupian( + this.data.xuanzhongTupian[i], + preGeneratedUrls[i], + i, + cosClient, + bucket, + region + ).then((result) => { + const currentDone = i + 1 + const jinduPercent = ((currentDone / total) * 100).toFixed(0) + this.setData({ shangchuanJindu: currentDone, jinduWidth: `${jinduPercent}%` }) + return result + }) + uploadTasks.push(task) + } + wx.showLoading({ title: `上传中...`, mask: true }) + await Promise.all(uploadTasks) + wx.hideLoading() + return preGeneratedUrls + }, + + tijiaoDingdan() { + this.showAnnouncementAndConfirm() + }, + + async showAnnouncementAndConfirm() { + try { + const res = await request({ + url: '/peizhi/tanchuang', + method: 'POST', + data: { pageKey: 'dsddxq' } + }) + if (res.statusCode !== 200 || res.data.code !== 0) { + this.showOriginalConfirmAndSubmit() + return + } + const { popups = [], serverTime } = res.data.data + if (!popups.length) { + this.showOriginalConfirmAndSubmit() + return + } + const popup = popups[0] + const popupComp = this.selectComponent('#popupNotice') + if (!popupComp) { + console.error('未找到弹窗组件 #popupNotice') + this.showOriginalConfirmAndSubmit() + return + } + const showData = { + popupId: popup.popup_id, + title: popup.title || '', + duration: popup.duration || 0, + images: popup.images || [], + showMuteCheckbox: false, + serverTime: serverTime + } + popupComp.show(showData, (result) => { + this.showOriginalConfirmAndSubmit() + }) + } catch (error) { + console.error('获取公告配置失败', error) + this.showOriginalConfirmAndSubmit() + } + }, + + showOriginalConfirmAndSubmit() { + if (this.data.isSubmitting) { + wx.showToast({ title: '正在提交中', icon: 'none' }) + return + } + if (this.data.jibenShuju.zhuangtai !== 2) { + wx.showToast({ title: '订单状态不可提交', icon: 'none' }) + return + } + if (this.data.xuanzhongTupian.length === 0) { + wx.showToast({ title: '请至少上传一张图片', icon: 'none' }) + return + } + wx.showModal({ + title: '⚠️ 提交确认', + content: '在提交订单之前,请确保您已在打手端页面认真阅读打手规则。如有漏交图片,订单被退款,概不申诉。', + confirmText: '我已阅读', + cancelText: '我再想想', + success: (res) => { + if (res.confirm) this._doSubmit() + }, + fail: (err) => { + console.error('弹窗失败', err) + wx.showToast({ title: '系统异常,请重试', icon: 'none' }) + } + }) + }, + + async _doSubmit() { + this.setData({ isSubmitting: true }) + wx.showLoading({ title: '开始提交...', mask: true }) + try { + const tupianUrls = await this.piliangShangchuanTupian() + if (!tupianUrls || tupianUrls.length === 0) throw new Error('无法生成上传路径') + wx.showLoading({ title: '提交订单数据...', mask: true }) + const res = await request({ + url: '/dingdan/dstijiao', + method: 'POST', + data: { + dingdan_id: this.data.jibenShuju.dingdan_id, + liuyan: this.data.liuyan || '', + jiaofu_tupian_urls: tupianUrls + }, + header: { 'content-type': 'application/json' } + }) + wx.hideLoading() + this.setData({ isSubmitting: false }) + if (res && res.data.code === 0) { + const ossImageUrl = app.globalData.ossImageUrl || '' + const fullUrls = tupianUrls.map(url => { + if (url && !url.startsWith('http')) { + if (url.startsWith('/')) url = url.substring(1) + return ossImageUrl + url + } + return url + }) + this.setData({ + 'jibenShuju.zhuangtai': 8, + 'xiangxiShuju.dashou_liuyan': this.data.liuyan || '', + 'xiangxiShuju.dashou_jiaofu': fullUrls, + xuanzhongTupian: [], + liuyan: '', + shangchuanJindu: 0, + shangchuanZongshu: 0, + jinduWidth: '0%', + }) + if (app.globalData.dashouzhuangtai != 1) app.globalData.dashouzhuangtai = 1 + wx.showToast({ title: '提交成功!', icon: 'success', duration: 2000 }) + setTimeout(() => wx.navigateBack(), 1500) + } else { + const errorMsg = res?.data?.msg || '提交失败' + wx.showToast({ title: errorMsg, icon: 'none' }) + } + } catch (error) { + console.error('提交失败:', error) + wx.hideLoading() + this.setData({ isSubmitting: false }) + wx.showToast({ title: error.message || '提交失败', icon: 'none' }) + } + }, + + // ========== 修改功能(增加最少一张图片校验) ========== + extractRelativePath(fullUrl) { + const ossImageUrl = this.data.ossImageUrl + if (fullUrl.startsWith(ossImageUrl)) return fullUrl.substring(ossImageUrl.length) + return fullUrl + }, + enterModify() { + const { dashou_jiaofu, dashou_liuyan } = this.data.xiangxiShuju + this.setData({ + isModifying: true, + originalDashouJiaofu: dashou_jiaofu.slice(), + remainingOriginals: dashou_jiaofu.slice(), + originalDashouLiuyan: dashou_liuyan || '', + modifiedLiuyan: dashou_liuyan || '', + deletedImages: [], + newImages: [] + }) + }, + cancelModify() { + this.setData({ + isModifying: false, + originalDashouJiaofu: [], + remainingOriginals: [], + originalDashouLiuyan: '', + modifiedLiuyan: '', + deletedImages: [], + newImages: [] + }) + }, + handleDeleteImage(e) { + const fullUrl = e.currentTarget.dataset.url + const { originalDashouJiaofu, remainingOriginals, deletedImages, newImages } = this.data + if (originalDashouJiaofu.includes(fullUrl)) { + const newRemaining = remainingOriginals.filter(url => url !== fullUrl) + this.setData({ + remainingOriginals: newRemaining, + deletedImages: [...deletedImages, fullUrl] + }) + } else { + const index = newImages.indexOf(fullUrl) + if (index > -1) { + const newImagesCopy = [...newImages] + newImagesCopy.splice(index, 1) + this.setData({ newImages: newImagesCopy }) + } + } + }, + handleAddImage() { + if (this.data.isConfirming) return + const currentCount = this.getCurrentImageCount() + const maxCount = 9 - currentCount + if (maxCount <= 0) { + wx.showToast({ title: '最多9张图片', icon: 'none' }) + return + } + wx.chooseImage({ + count: maxCount, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: (res) => { + const newImages = [...this.data.newImages, ...res.tempFilePaths] + this.setData({ newImages: newImages.slice(0, 9) }) + } + }) + }, + getCurrentImageCount() { + const { remainingOriginals, newImages } = this.data + return remainingOriginals.length + newImages.length + }, + onModifyLiuyanInput(e) { + this.setData({ modifiedLiuyan: e.detail.value.slice(0, 50) }) + }, + + async confirmModify() { + if (this.data.isConfirming) return + const { dingdan_id } = this.data.jibenShuju + const { remainingOriginals, deletedImages, newImages, modifiedLiuyan, originalDashouLiuyan } = this.data + const hasLiuyanChange = modifiedLiuyan !== originalDashouLiuyan + const hasDelete = deletedImages.length > 0 + const hasAdd = newImages.length > 0 + + if (!hasLiuyanChange && !hasDelete && !hasAdd) { + wx.showToast({ title: '没有修改内容', icon: 'none' }) + return + } + + const finalImageCount = remainingOriginals.length + newImages.length + if (finalImageCount === 0) { + wx.showToast({ title: '至少保留一张图片', icon: 'none' }) + return + } + + this.setData({ isConfirming: true }) + wx.showLoading({ title: '提交修改...', mask: true }) + try { + const tokenData = await this.getCOSZhengshu() + const { cos, bucket, region } = this.initCOSClient(tokenData) + + let newImageRelativePaths = [] + if (hasAdd) { + const total = newImages.length + this.setData({ 'modifyUploadProgress.total': total, 'modifyUploadProgress.current': 0, 'modifyUploadProgress.width': '0%' }) + const preGeneratedKeys = [] + for (let i = 0; i < total; i++) { + const timestamp = Date.now() + i + const random = Math.floor(Math.random() * 10000) + const fileExt = this.getFileExtension(newImages[i]) + const fileName = `${timestamp}_${random}${fileExt}` + const cosKey = `order/${this.data.jibenShuju.dingdan_id}/${fileName}` + preGeneratedKeys.push(cosKey) + } + const uploadTasks = [] + for (let i = 0; i < total; i++) { + const task = this.shangchuanDanZhangTupian( + newImages[i], + preGeneratedKeys[i], + i, + cos, + bucket, + region + ).then((result) => { + const currentDone = i + 1 + const jinduPercent = ((currentDone / total) * 100).toFixed(0) + this.setData({ + 'modifyUploadProgress.current': currentDone, + 'modifyUploadProgress.width': `${jinduPercent}%` + }) + return result.key + }) + uploadTasks.push(task) + } + wx.showLoading({ title: `上传中...`, mask: true }) + newImageRelativePaths = await Promise.all(uploadTasks) + wx.hideLoading() + this.setData({ 'modifyUploadProgress.total': 0, 'modifyUploadProgress.current': 0, 'modifyUploadProgress.width': '0%' }) + } + + const deletedRelativePaths = deletedImages.map(fullUrl => this.extractRelativePath(fullUrl)) + + const res = await request({ + url: '/dingdan/dsxiugaidd', + method: 'POST', + data: { + dingdan_id, + liuyan: modifiedLiuyan, + deleted_images: deletedRelativePaths, + new_images: newImageRelativePaths + }, + header: { 'content-type': 'application/json' } + }) + wx.hideLoading() + this.setData({ isConfirming: false }) + if (res && res.data.code === 0) { + const ossImageUrl = this.data.ossImageUrl + const newFullUrls = newImageRelativePaths.map(rel => ossImageUrl + rel) + const newDashouJiaofu = [...remainingOriginals, ...newFullUrls] + this.setData({ + 'xiangxiShuju.dashou_jiaofu': newDashouJiaofu, + 'xiangxiShuju.dashou_liuyan': modifiedLiuyan, + isModifying: false, + originalDashouJiaofu: [], + remainingOriginals: [], + originalDashouLiuyan: '', + deletedImages: [], + newImages: [], + modifiedLiuyan: '' + }) + wx.showToast({ title: '修改成功', icon: 'success' }) + } else { + const errorMsg = res?.data?.msg || '修改失败' + wx.showToast({ title: errorMsg, icon: 'none' }) + } + } catch (error) { + console.error('修改失败:', error) + wx.hideLoading() + this.setData({ isConfirming: false }) + wx.showToast({ title: error.message || '修改失败', icon: 'none' }) + } + } +}) \ No newline at end of file diff --git a/pages/dsddxq/dsddxq.json b/pages/dsddxq/dsddxq.json new file mode 100644 index 0000000..837f016 --- /dev/null +++ b/pages/dsddxq/dsddxq.json @@ -0,0 +1,5 @@ +{ + "usingComponents": { + "global-notification": "/components/global-notification/global-notification" + } +} \ No newline at end of file diff --git a/pages/dsddxq/dsddxq.wxml b/pages/dsddxq/dsddxq.wxml new file mode 100644 index 0000000..7a715d6 --- /dev/null +++ b/pages/dsddxq/dsddxq.wxml @@ -0,0 +1,478 @@ + + + + + + 加载订单详情... + + + + + + + + + + + + + {{jibenShuju.jieshao || '暂无描述'}} + + + 点击上面字体查看完整介绍 + + + + + + + 订单信息 + + + + 游戏昵称: + {{jibenShuju.nicheng}} + + 📋 + 复制 + + + + + + 备注信息: + + {{jibenShuju.beizhu.length > 20 ? jibenShuju.beizhu.substring(0, 20) + '...' : jibenShuju.beizhu}} + + 查看完整 + + + + + + + 订单ID: + + {{jibenShuju.dingdan_id}} + + 📋 + 复制 + + + + + + + 创建时间: + {{jibenShuju.create_time}} + + + + + 订单价格: + ¥{{jibenShuju.jine}} + + + + + 订单状态: + {{zhuangtaiWenziMap[jibenShuju.zhuangtai] || '未知状态'}} + + + + + 发单平台: + + {{jibenShuju.fadanpingtai == 1 ? '平台发单' : (jibenShuju.fadanpingtai == 2 ? '商家发单' : '未知')}} + + + + + + + 商家信息 + + + + 商家ID: + + {{xiangxiShuju.shangjia_id}} + + 📋 + 复制 + + + + + + + 商家昵称: + {{xiangxiShuju.shangjia_nicheng}} + + + + + 商家留言 + + {{xiangxiShuju.shangjia_liuyan}} + + + + + + + 退款信息 + + + 退款理由: + {{xiangxiShuju.tuikuan_liyou}} + + + + + + 处罚申请 + + + + 申请原因: + {{xiangxiShuju.chufa_liyou}} + + + + + 申请状态: + + {{xiangxiShuju.chufa_zhuangtai == 0 ? '审核中' : (xiangxiShuju.chufa_zhuangtai == 1 ? '处罚成功' : '处罚驳回')}} + + + + + + 处罚结果: + {{xiangxiShuju.chufa_jieguo}} + + + + + 驳回理由: + {{xiangxiShuju.bohui_liyou}} + + + + + + 我的交付 + + + + 交付图片 + + + + + + + + + + + + 我的留言 + + {{xiangxiShuju.dashou_liuyan}} + + + + + + + 修改交付 + + + + 交付图片(点击叉号删除) + + + + + + + + + × + + + + + + + + + + × + + + + + + + + + 添加图片 + ({{9 - (remainingOriginals.length + newImages.length)}}/9) + + + + + + + 留言(可选,最多50字) + + + 已输入 {{modifiedLiuyan.length}}/50 字 + + + + + + + 提交订单 + + + + 选择交付图片(最多9张) + + + + + + + + × + + + + + + + + + 添加图片 + ({{9 - xuanzhongTupian.length}}/9) + + + + + + + + + + 上传中: {{shangchuanJindu}}/{{shangchuanZongshu}} + + + + + 留言(可选,最多50字) + + + 已输入 {{liuyan.length}}/50 字 + + + + + + + + + + + + + 取消修改 + + + {{isConfirming ? '提交中...' : '确认修改'}} + + + + + + + + + + 联系客服 + + + 联系老板 + + + + + + 联系客服 + + + + + + + 修改交付 + + + + + + {{isSubmitting ? '提交中...' : '立即提交'}} + + + + + + + + + + + + 订单介绍 + × + + + + {{wanzhengJieshao}} + + + + + + + + + + + 订单备注 + × + + + + {{wanzhengBeizhu}} + + + + + + + + \ No newline at end of file diff --git a/pages/dsddxq/dsddxq.wxss b/pages/dsddxq/dsddxq.wxss new file mode 100644 index 0000000..7e9a205 --- /dev/null +++ b/pages/dsddxq/dsddxq.wxss @@ -0,0 +1,589 @@ +/* pages/dsddxq/dsddxq.wxss */ + +/* 页面基础样式 */ +.dsddxq-page { + min-height: 100vh; + background-color: #f8f9fa; + position: relative; + box-sizing: border-box; +} + +/* 加载中状态 */ +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; +} + +.loading-spinner { + width: 80rpx; + height: 80rpx; + border: 8rpx solid #e0e0e0; + border-top-color: #2196f3; + border-radius: 50%; + animation: spin 1.2s linear infinite; + margin-bottom: 30rpx; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.loading-text { + font-size: 30rpx; + color: #666666; +} + +/* 内容区域 */ +.content-container { + padding: 25rpx 25rpx 220rpx; /* 减少底部内边距,避免过多空白 */ +} + +/* 商品图片和介绍区域 */ +.shangpin-area { + display: flex; + background-color: #ffffff; + border-radius: 20rpx; + padding: 25rpx; + margin-bottom: 25rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06); +} + +.shangpin-image-container { + flex-shrink: 0; + width: 180rpx; + height: 180rpx; + margin-right: 25rpx; +} + +.shangpin-image { + width: 100%; + height: 100%; + border-radius: 12rpx; + background-color: #f5f5f5; +} + +.shangpin-jieshao { + flex: 1; + display: flex; + flex-direction: column; +} + +.jieshao-text { + font-size: 28rpx; + line-height: 42rpx; + color: #333333; + font-weight: 400; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + overflow: hidden; + text-overflow: ellipsis; +} + +.chakan-wanzheng { + margin-top: 10rpx; +} + +.chakan-wanzheng text { + font-size: 24rpx; + color: #2196f3; + text-decoration: underline; +} + +/* 通用区域样式 */ +.info-title { + font-size: 32rpx; + color: #333333; + font-weight: 600; + margin-bottom: 20rpx; + padding-bottom: 12rpx; + border-bottom: 2rpx solid rgba(0, 0, 0, 0.08); +} + +.section-subtitle { + font-size: 28rpx; + color: #555555; + font-weight: 500; + display: block; + margin: 15rpx 0 10rpx; +} + +/* 订单信息区域 */ +.dingdan-info-area, +.shangjia-area, +.tuikuan-area, +.chufa-area, +.dashou-jiaofu-area, +.tijiao-area { + background-color: #ffffff; + border-radius: 20rpx; + padding: 25rpx; + margin-bottom: 25rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06); +} + +.info-item-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 18rpx; + padding: 12rpx 0; +} + +.info-item-row:last-child { + margin-bottom: 0; +} + +.item-label { + font-size: 28rpx; + color: #666666; + font-weight: 400; + flex-shrink: 0; + min-width: 150rpx; +} + +/* 右侧区域容器 */ +.item-right-area { + flex: 1; + display: flex; + align-items: center; + justify-content: flex-end; +} + +.item-value { + font-size: 28rpx; + color: #333333; + font-weight: 500; + word-break: break-all; + text-align: right; + margin-right: 15rpx; +} + +/* 特殊样式 */ +.jine-green { + color: #4caf50 !important; + font-weight: 600 !important; + font-size: 32rpx !important; +} + +.zhuangtai-text { + font-weight: 600 !important; + font-size: 30rpx !important; + color: #333333 !important; +} + +/* 按钮样式(复制、查看) */ +.copy-btn, +.chakan-btn { + display: flex; + align-items: center; + justify-content: center; + font-size: 24rpx; + color: #4caf50; + background-color: #e8f5e9; + padding: 6rpx 16rpx; + border-radius: 20rpx; + border: 1rpx solid #4caf50; + transition: all 0.2s ease; + white-space: nowrap; + flex-shrink: 0; +} + +.chakan-btn { + color: #2196f3; + background-color: #e3f2fd; + border-color: #2196f3; + margin-left: 10rpx; +} + +.copy-btn-hover, +.chakan-btn:active { + background-color: #c8e6c9; + transform: scale(0.95); +} + +.copy-icon { + font-size: 20rpx; + margin-right: 6rpx; +} + +.copy-text { + font-weight: 500; +} + +/* 打手交付图片网格 */ +.jiaofu-grid { + display: flex; + flex-wrap: wrap; + margin: -5rpx; +} + +.jiaofu-image-item { + width: calc(33.333% - 10rpx); + margin: 5rpx; + aspect-ratio: 1/1; +} + +.jiaofu-image { + width: 100%; + height: 100%; + border-radius: 12rpx; + background-color: #f5f5f5; + transition: transform 0.2s ease; +} + +.jiaofu-image:active { + transform: scale(0.95); +} + +/* 留言内容样式 */ +.liuyan-content { + font-size: 26rpx; + line-height: 40rpx; + color: #555555; + padding: 15rpx; + background-color: #f9f9f9; + border-radius: 12rpx; + margin-top: 8rpx; +} + +/* 选择图片区域 */ +.yixuan-tupian-grid { + display: flex; + flex-wrap: wrap; + margin: -8rpx; +} + +.yixuan-tupian-item { + width: calc(33.333% - 16rpx); + margin: 8rpx; + aspect-ratio: 1/1; + position: relative; +} + +.yixuan-tupian { + width: 100%; + height: 100%; + border-radius: 12rpx; + background-color: #f5f5f5; +} + +.shanchu-btn { + position: absolute; + top: -10rpx; + right: -10rpx; + width: 40rpx; + height: 40rpx; + background-color: #ff4444; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 24rpx; + font-weight: bold; + z-index: 10; +} + +.tianjia-btn { + width: calc(33.333% - 16rpx); + margin: 8rpx; + aspect-ratio: 1/1; + background-color: #f5f5f5; + border-radius: 12rpx; + border: 2rpx dashed #cccccc; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #666666; +} + +.tianjia-icon { + font-size: 48rpx; + font-weight: 300; + margin-bottom: 10rpx; +} + +.tianjia-text { + font-size: 24rpx; + margin-bottom: 4rpx; +} + +.tianjia-tip { + font-size: 20rpx; + color: #999999; +} + +/* 上传进度 */ +.shangchuan-jindu-container { + margin-top: 20rpx; +} + +.jindu-bar { + width: 100%; + height: 20rpx; + background-color: #f0f0f0; + border-radius: 10rpx; + overflow: hidden; + margin-bottom: 10rpx; +} + +.jindu-fill { + height: 100%; + background-color: #4caf50; + border-radius: 10rpx; + transition: width 0.3s ease; +} + +.jindu-text { + font-size: 24rpx; + color: #666666; + text-align: center; + display: block; +} + +/* 留言输入 */ +.liuyan-textarea { + width: 100%; + min-height: 120rpx; + font-size: 28rpx; + color: #333333; + background-color: #f9f9f9; + border-radius: 12rpx; + padding: 20rpx; + margin-top: 10rpx; + box-sizing: border-box; +} + +.zi-shu { + display: block; + text-align: right; + font-size: 24rpx; + color: #999999; + margin-top: 8rpx; +} + +/* ==================== 底部按钮区域 ==================== */ +.bottom-btns-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: #ffffff; + padding: 20rpx 30rpx; + border-top: 1rpx solid #f0f0f0; + box-shadow: 0 -2rpx 20rpx rgba(0, 0, 0, 0.08); + z-index: 100; + display: flex; + flex-direction: column; + gap: 10rpx; /* 减小按钮间距 */ +} + +/* 🆕 新增:按钮行布局(用于并排两个按钮) */ +.btn-row { + display: flex; + justify-content: space-between; + width: 100%; +} + +/* 🆕 当有两个按钮时,各占一半(减去gap的一半) */ +.btn-row .bottom-btn { + width: calc(50% - 10rpx); +} + +/* 🆕 当只有一个按钮时,占满 */ +.btn-row .full-width { + width: 100%; +} + +/* 统一底部按钮样式 */ +.bottom-btn { + width: 100%; + height: 88rpx; /* 统一高度 */ + border-radius: 44rpx; /* 圆角 */ + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: 600; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + border: none; + box-sizing: border-box; +} + +.btn-hover { + opacity: 0.9; + transform: scale(0.98); +} + +/* 联系客服按钮 */ +.contact-btn { + background-color: #2196F3; + color: #ffffff; + box-shadow: 0 4rpx 15rpx rgba(33, 150, 243, 0.3); +} + +/* 🆕 新增:联系老板按钮 */ +.boss-btn { + background-color: #ff9800; + color: #ffffff; + box-shadow: 0 4rpx 15rpx rgba(255, 152, 0, 0.3); +} + +/* 🆕 新增:修改按钮(黑色) */ +.modify-btn { + background-color: #333333; + color: #ffffff; + box-shadow: 0 4rpx 15rpx rgba(51, 51, 51, 0.3); +} + +/* 🆕 新增:取消修改按钮 */ +.cancel-modify-btn { + background-color: #9e9e9e; + color: #ffffff; + box-shadow: 0 4rpx 15rpx rgba(158, 158, 158, 0.3); +} + +/* 🆕 新增:确认修改按钮 */ +.confirm-modify-btn { + background-color: #4caf50; + color: #ffffff; + box-shadow: 0 4rpx 15rpx rgba(76, 175, 80, 0.3); +} + +/* 提交按钮 */ +.submit-btn { + background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%); + color: #ffffff; + box-shadow: 0 4rpx 15rpx rgba(76, 175, 80, 0.3); +} + +/* 提交按钮闪光特效 */ +.submit-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + animation: flash 2s infinite; +} + +@keyframes flash { + 0% { left: -100%; } + 100% { left: 100%; } +} + +.btn-text { + position: relative; + z-index: 1; +} + +/* ==================== 弹窗样式 ==================== */ +.modal-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); +} + +.modal-content { + position: relative; + width: 600rpx; + max-height: 70vh; + background-color: #ffffff; + border-radius: 20rpx; + overflow: hidden; + box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.15); + z-index: 1001; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 30rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.modal-title { + font-size: 34rpx; + color: #333333; + font-weight: 600; +} + +.modal-close { + font-size: 40rpx; + color: #999999; + width: 40rpx; + height: 40rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-body { + padding: 30rpx; + max-height: 50vh; + overflow: hidden; +} + +.modal-scroll { + height: 100%; + max-height: 50vh; +} + +.modal-content-text { + font-size: 28rpx; + line-height: 44rpx; + color: #333333; + white-space: pre-wrap; +} + +/* 响应式调整 */ +@media (max-width: 375px) { + .shangpin-image-container { + width: 160rpx; + height: 160rpx; + } + + .bottom-btn { + height: 80rpx; + font-size: 30rpx; + } + + .item-label { + min-width: 140rpx; + font-size: 26rpx; + } + + .item-value { + font-size: 26rpx; + } + + .modal-content { + width: 560rpx; + } +} \ No newline at end of file diff --git a/pages/fenlei/fenlei.js b/pages/fenlei/fenlei.js new file mode 100644 index 0000000..9cbf998 --- /dev/null +++ b/pages/fenlei/fenlei.js @@ -0,0 +1,452 @@ +// pages/fenlei/fenlei.js +const app = getApp() + +Page({ + /** + * 页面的初始数据 + */ + data: { + // 全局变量引用 + ossImageUrl: '', + + // 搜索相关 + searchText: '', + showSearchResult: false, + searchResults: [], + isSearching: false, + + // 页面数据 + shangpinleixing: [], + shangpinzhuanqu: [], + shangpinliebiao: [], + + // 状态控制 + selectedLeixingId: null, + selectedZhuanquId: null, + filteredZhuanquList: [], // 当前类型下的专区列表 + filteredShangpinList: [], // 当前专区下的商品列表 + zhuanquByLeixing: {}, // 类型->专区映射对象 + shangpinByZhuanqu: {}, // 专区->商品映射对象 + zhuanquGoodsCount: {}, // 专区商品数量统计 + + // UI状态 + isLoading: false, + hasInitialized: false + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + // 获取全局变量 + this.setData({ + ossImageUrl: app.globalData.ossImageUrl + }) + + // 初始化数据 + this.initPageData() + }, + onShow() { + // 原有代码... + + // 🆕 注册通知组件 + this.registerNotificationComponent(); +}, + +// 🆕 新增:注册通知组件 +registerNotificationComponent() { + const app = getApp(); + const notificationComp = this.selectComponent('#global-notification'); + + if (notificationComp && notificationComp.showNotification) { + console.log('🏪 商城页面注册通知组件'); + app.globalData.globalNotification = { + show: (data) => notificationComp.showNotification(data), + hide: () => notificationComp.hideNotification() + }; + } +}, + + /** + * 初始化页面数据 + */ + initPageData() { + const that = this + this.setData({ isLoading: true }) + + // 检查全局变量是否已有数据 + if (app.globalData.shangpinleixing && app.globalData.shangpinleixing.length > 0) { + this.initFromGlobalData() + } else { + this.loadAllShangpinData() + } + }, + + /** + * 从全局变量初始化数据 + */ + initFromGlobalData() { + const leixingList = app.globalData.shangpinleixing || [] + const zhuanquList = app.globalData.shangpinzhuanqu || [] + const shangpinList = app.globalData.shangpinliebiao || [] + + // 构建映射关系对象 + const zhuanquByLeixing = {} + const shangpinByZhuanqu = {} + const zhuanquGoodsCount = {} + + // 1. 构建专区映射 (按类型分组) + zhuanquList.forEach(zhuanqu => { + const leixingId = zhuanqu.leixing_id + if (!zhuanquByLeixing[leixingId]) { + zhuanquByLeixing[leixingId] = [] + } + zhuanquByLeixing[leixingId].push(zhuanqu) + }) + + // 2. 构建商品映射 (按专区分组)并统计数量 + shangpinList.forEach(shangpin => { + const zhuanquId = shangpin.zhuanqu_id + if (zhuanquId) { + if (!shangpinByZhuanqu[zhuanquId]) { + shangpinByZhuanqu[zhuanquId] = [] + zhuanquGoodsCount[zhuanquId] = 0 + } + + // 处理价格显示 + const jiage = parseFloat(shangpin.jiage) || 0 + const priceInteger = Math.floor(jiage) + let priceDecimal = '00' + + const decimalPart = (jiage - priceInteger).toFixed(2) + if (decimalPart > 0) { + priceDecimal = decimalPart.toString().split('.')[1] + } + + shangpinByZhuanqu[zhuanquId].push({ + id: shangpin.id, + biaoqian: shangpin.biaoqian, + jiage: jiage, + priceInteger: priceInteger, + priceDecimal: priceDecimal, + tupian_url: shangpin.tupian_url, + duiwai_xiaoliang: shangpin.duiwai_xiaoliang || 0 + }) + + zhuanquGoodsCount[zhuanquId] += 1 + } + }) + + // 设置初始选中状态(第一个类型和第一个专区) + const firstLeixing = leixingList[0] + let firstZhuanquId = null + + if (firstLeixing && zhuanquByLeixing[firstLeixing.id]) { + firstZhuanquId = zhuanquByLeixing[firstLeixing.id][0]?.id || null + } + + this.setData({ + shangpinleixing: leixingList, + shangpinzhuanqu: zhuanquList, + shangpinliebiao: shangpinList, + zhuanquByLeixing: zhuanquByLeixing, + shangpinByZhuanqu: shangpinByZhuanqu, + zhuanquGoodsCount: zhuanquGoodsCount, + selectedLeixingId: firstLeixing?.id || null, + selectedZhuanquId: firstZhuanquId + }, () => { + // 筛选数据 + this.filterData() + this.setData({ isLoading: false, hasInitialized: true }) + }) + }, + + /** + * 加载所有商品数据 + */ + loadAllShangpinData() { + const that = this + wx.request({ + url: app.globalData.apiBaseUrl + '/shangpin/shangpinhuoqu/', + method: 'POST', + header: { + 'content-type': 'application/json' + }, + success(res) { + if (res.statusCode === 200 && res.data) { + const data = res.data + + // 存储到全局变量 + app.globalData.shangpinleixing = data.shangpinleixing || [] + app.globalData.shangpinzhuanqu = data.shangpinzhuanqu || [] + app.globalData.shangpinliebiao = data.shangpinliebiao || [] + + // 从全局变量初始化 + that.initFromGlobalData() + } else { + that.handleLoadError() + } + }, + fail(error) { + console.error('加载商品数据失败:', error) + that.handleLoadError() + } + }) + }, + + /** + * 处理加载错误 + */ + handleLoadError() { + wx.showToast({ + title: '加载失败,请重试', + icon: 'none' + }) + this.setData({ isLoading: false }) + }, + + /** + * 筛选数据 + */ + filterData() { + const { selectedLeixingId, selectedZhuanquId, zhuanquByLeixing, shangpinByZhuanqu } = this.data + + // 1. 筛选专区列表 + const filteredZhuanquList = zhuanquByLeixing[selectedLeixingId] || [] + + // 2. 筛选商品列表 + let filteredShangpinList = [] + if (selectedZhuanquId) { + filteredShangpinList = shangpinByZhuanqu[selectedZhuanquId] || [] + } else if (filteredZhuanquList.length > 0) { + // 如果没有选中的专区,默认显示第一个专区的商品 + const firstZhuanquId = filteredZhuanquList[0].id + filteredShangpinList = shangpinByZhuanqu[firstZhuanquId] || [] + this.setData({ selectedZhuanquId: firstZhuanquId }) + } + + this.setData({ + filteredZhuanquList: filteredZhuanquList, + filteredShangpinList: filteredShangpinList + }) + }, + + /** + * 选择商品类型 + */ + selectLeixing(e) { + const leixingId = e.currentTarget.dataset.id + + if (this.data.selectedLeixingId === leixingId) { + return + } + + // 获取该类型下的第一个专区 + const zhuanquList = this.data.zhuanquByLeixing[leixingId] || [] + const firstZhuanquId = zhuanquList.length > 0 ? zhuanquList[0].id : null + + this.setData({ + selectedLeixingId: leixingId, + selectedZhuanquId: firstZhuanquId + }, () => { + this.filterData() + }) + }, + + /** + * 选择商品专区 + */ + selectZhuanqu(e) { + const zhuanquId = e.currentTarget.dataset.id + + if (this.data.selectedZhuanquId === zhuanquId) { + return + } + + this.setData({ + selectedZhuanquId: zhuanquId + }, () => { + // 筛选商品列表 + const filteredShangpinList = this.data.shangpinByZhuanqu[zhuanquId] || [] + this.setData({ + filteredShangpinList: filteredShangpinList + }) + }) + }, + + /** + * 搜索框输入事件 + */ + onSearchInput(e) { + this.setData({ + searchText: e.detail.value + }) + }, + + /** + * 点击搜索按钮 + */ + onSearchTap() { + const searchText = this.data.searchText.trim() + if (!searchText) { + wx.showToast({ + title: '请输入搜索内容', + icon: 'none' + }) + return + } + + // 开始搜索 + this.startSearch(searchText) + }, + + /** + * 键盘搜索事件(回车键) + */ + onSearchConfirm(e) { + const searchText = e.detail.value.trim() + if (!searchText) { + wx.showToast({ + title: '请输入搜索内容', + icon: 'none' + }) + return + } + + this.setData({ + searchText: searchText + }) + + // 开始搜索 + this.startSearch(searchText) + }, + + /** + * 执行搜索 + */ + startSearch(searchText) { + this.setData({ + isSearching: true, + showSearchResult: true + }) + + // 本地搜索 + const allShangpin = this.data.shangpinliebiao + const results = [] + + for (const item of allShangpin) { + if (item.biaoqian && item.biaoqian.toLowerCase().includes(searchText.toLowerCase())) { + // 处理价格显示 + const jiage = parseFloat(item.jiage) || 0 + const priceInteger = Math.floor(jiage) + let priceDecimal = '00' + + const decimalPart = (jiage - priceInteger).toFixed(2) + if (decimalPart > 0) { + priceDecimal = decimalPart.toString().split('.')[1] + } + + results.push({ + id: item.id, + biaoqian: item.biaoqian, + jiage: jiage, + priceInteger: priceInteger, + priceDecimal: priceDecimal, + tupian_url: item.tupian_url, + leixing_name: this.getLeixingName(item.leixing_id), + zhuanqu_name: this.getZhuanquName(item.zhuanqu_id) + }) + } + } + + this.setData({ + searchResults: results, + isSearching: false + }) + }, + + /** + * 清空搜索 + */ + onClearSearch() { + this.setData({ + searchText: '', + showSearchResult: false, + searchResults: [] + }) + }, + + /** + * 根据类型ID获取类型名称 + */ + getLeixingName(leixingId) { + const leixingList = this.data.shangpinleixing + for (const item of leixingList) { + if (item.id === leixingId) { + return item.jieshao + } + } + return '' + }, + + /** + * 根据专区ID获取专区名称 + */ + getZhuanquName(zhuanquId) { + const zhuanquList = this.data.shangpinzhuanqu + for (const item of zhuanquList) { + if (item.id === zhuanquId) { + return item.mingzi + } + } + return '' + }, + + /** + * 跳转到商品详情页 + */ + goToDetail(e) { + const shangpinId = e.currentTarget.dataset.id + if (!shangpinId) return + + wx.navigateTo({ + url: `/pages/shangpinxiangqing/shangpinxiangqing?id=${shangpinId}` + }) + }, + + /** + * 刷新所有数据 + */ + onRefresh() { + wx.showLoading({ + title: '刷新中...', + mask: true + }) + + // 清空全局变量 + app.globalData.shangpinleixing = [] + app.globalData.shangpinzhuanqu = [] + app.globalData.shangpinliebiao = [] + + this.loadAllShangpinData() + + setTimeout(() => { + wx.hideLoading() + wx.showToast({ + title: '刷新成功', + icon: 'success', + duration: 1500 + }) + }, 1000) + }, + + /** + * 用户点击右上角分享 + */ + onShareAppMessage() { + return { + title: '阿龙电竞 - 商品分类', + path: '/pages/fenlei/fenlei' + } + } +}) \ No newline at end of file diff --git a/pages/fenlei/fenlei.json b/pages/fenlei/fenlei.json new file mode 100644 index 0000000..4af9065 --- /dev/null +++ b/pages/fenlei/fenlei.json @@ -0,0 +1,6 @@ +{ + "usingComponents": { + "global-notification": "/components/global-notification/global-notification", + "custom-tab-bar": "/custom-tab-bar/index" + } + } \ No newline at end of file diff --git a/pages/fenlei/fenlei.wxml b/pages/fenlei/fenlei.wxml new file mode 100644 index 0000000..6f015fa --- /dev/null +++ b/pages/fenlei/fenlei.wxml @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + 搜索 + + + + + + + + + + + + + + + {{item.jieshao}} + + + + + + + + + + + + + + + + {{item.mingzi}} + ({{zhuanquGoodsCount[item.id] || 0}}) + + + + + + + + 暂无专区 + + + + + + + + + + + + + + + + {{item.biaoqian}} + + + + + ¥ + {{item.priceInteger}} + .{{item.priceDecimal}} + + + 了解详情 + + + + + + + + + + + 该专区暂无商品 + + + + + — 已加载全部商品 — + + + + + + + + + + + 搜索结果 + ({{searchResults.length}}个) + + + + + + + + + + + + + + {{item.biaoqian}} + + {{item.leixing_name}} + · + {{item.zhuanqu_name}} + + + ¥ + {{item.priceInteger}} + .{{item.priceDecimal}} + + + + + + + + 未找到相关商品 + + + + + + + 搜索中... + + + + + + + + + + + + + 加载中... + + + + + + + \ No newline at end of file diff --git a/pages/fenlei/fenlei.wxss b/pages/fenlei/fenlei.wxss new file mode 100644 index 0000000..d4a3420 --- /dev/null +++ b/pages/fenlei/fenlei.wxss @@ -0,0 +1,1215 @@ +/* pages/fenlei/fenlei.wxss */ +.fenlei-page { + width: 100%; + height: 100vh; + background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%); + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + } + + /* 1. 毛玻璃搜索框区域(固定) */ + .search-container { + padding: 24rpx 32rpx; + background: transparent; + flex-shrink: 0; + position: relative; + z-index: 100; + } + + .search-box { + display: flex; + align-items: center; + background: rgba(255, 255, 255, 0.85); + backdrop-filter: blur(30rpx); + -webkit-backdrop-filter: blur(30rpx); + border-radius: 50rpx; + padding: 0 28rpx; + height: 84rpx; + box-shadow: + 0 8rpx 32rpx rgba(31, 38, 135, 0.1), + inset 0 1rpx 0 rgba(255, 255, 255, 0.8); + border: 1rpx solid rgba(255, 255, 255, 0.3); + position: relative; + overflow: hidden; + } + + .search-box::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.1)); + border-radius: 50rpx; + z-index: -1; + } + + .search-icon { + width: 34rpx; + height: 34rpx; + margin-right: 18rpx; + opacity: 0.6; + } + + .search-input { + flex: 1; + height: 100%; + font-size: 28rpx; + color: #222; + font-weight: 400; + } + + .search-placeholder { + color: #aaa; + font-size: 28rpx; + } + + .search-btn { + background: linear-gradient(135deg, #ffcc00, #ffaa00); + border-radius: 40rpx; + padding: 14rpx 32rpx; + margin-left: 20rpx; + position: relative; + overflow: hidden; + box-shadow: 0 6rpx 20rpx rgba(255, 170, 0, 0.3); + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + } + + .search-btn:active { + transform: scale(0.95); + box-shadow: 0 4rpx 15rpx rgba(255, 170, 0, 0.4); + } + + .search-btn-text { + font-size: 26rpx; + color: white; + font-weight: 600; + position: relative; + z-index: 1; + } + + .search-btn-glow { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent); + transform: translateX(-100%); + transition: transform 0.6s; + } + + .search-btn:active .search-btn-glow { + transform: translateX(100%); + } + + .search-clear { + width: 36rpx; + height: 36rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-left: 16rpx; + opacity: 0.6; + transition: opacity 0.3s; + } + + .search-clear:active { + opacity: 1; + } + + .clear-icon { + width: 24rpx; + height: 24rpx; + } + + /* 2. 商品类型选择区域(固定)- 恢复原大小 */ + .leixing-container { + padding: 0 32rpx 24rpx; + flex-shrink: 0; + position: relative; + } + + .leixing-bg-glow { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 300rpx; + height: 300rpx; + background: radial-gradient(circle, rgba(255, 204, 0, 0.1) 0%, transparent 70%); + z-index: -1; + } + + .leixing-scroll { + white-space: nowrap; + height: 180rpx; /* 恢复原大小 */ + } + + .leixing-list { + display: inline-flex; + padding-right: 32rpx; + } + + .leixing-item { + display: inline-flex; + flex-direction: column; + align-items: center; + margin-right: 48rpx; /* 增加间距 */ + position: relative; + padding: 12rpx; + } + + .leixing-image-box { + width: 100rpx; /* 恢复原大小 */ + height: 100rpx; /* 恢复原大小 */ + border-radius: 50%; + position: relative; + margin-bottom: 16rpx; + overflow: hidden; + background: white; + box-shadow: + 0 12rpx 30rpx rgba(0, 0, 0, 0.12), + 0 4rpx 8rpx rgba(0, 0, 0, 0.06); + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + } + + .leixing-active .leixing-image-box { + transform: scale(1.12); + box-shadow: + 0 18rpx 40rpx rgba(255, 204, 0, 0.3), + 0 8rpx 16rpx rgba(255, 170, 0, 0.2); + } + + .leixing-image { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + } + + .leixing-glow { + position: absolute; + top: -3rpx; + left: -3rpx; + right: -3rpx; + bottom: -3rpx; + border-radius: 50%; + background: linear-gradient(135deg, #ffcc00, #ffaa00, #ff8800); + opacity: 0; + transition: opacity 0.4s; + z-index: -1; + filter: blur(10rpx); + } + + .leixing-glow-active { + opacity: 0.6; + animation: glowPulse 2s infinite; + } + + .leixing-sparkle { + position: absolute; + top: 50%; + left: 50%; + width: 20rpx; + height: 20rpx; + background: white; + border-radius: 50%; + transform: translate(-50%, -50%) scale(0); + opacity: 0; + pointer-events: none; + } + + .sparkle-animation { + animation: sparkleFlash 1.5s infinite; + } + + .leixing-text { + font-size: 24rpx; + color: #555; /* 颜色加深 */ + font-weight: 500; + transition: all 0.3s; + white-space: nowrap; + } + + .leixing-active .leixing-text { + color: #ff9900; + font-weight: 600; + transform: translateY(-2rpx); + text-shadow: 0 2rpx 4rpx rgba(255, 153, 0, 0.2); + } + + .leixing-active-bar { + position: absolute; + bottom: -4rpx; + width: 44rpx; + height: 4rpx; + background: linear-gradient(90deg, #ffcc00, #ffaa00); + border-radius: 2rpx; + opacity: 0; + transform: scaleX(0); + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + box-shadow: 0 2rpx 8rpx rgba(255, 170, 0, 0.4); + } + + .leixing-active-bar-show { + opacity: 1; + transform: scaleX(1); + } + + .leixing-border-effect { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 50%; + border: 2rpx solid transparent; + opacity: 0; + pointer-events: none; + } + + .border-effect-active { + opacity: 1; + border-color: rgba(255, 204, 0, 0.5); + animation: borderGlow 2s infinite; + } + + /* 3. 左右分栏布局(可滚动区域) */ + .main-container { + flex: 1; + display: flex; + height: calc(100vh - 300rpx); + overflow: hidden; + } + + /* 左边:商品专区列表 */ + .zhuanqu-left { + width: 200rpx; + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(10rpx); + border-right: 1rpx solid rgba(0, 0, 0, 0.08); + flex-shrink: 0; + position: relative; + } + + .zhuanqu-list { + padding: 0; + } + + .zhuanqu-item { + position: relative; + padding: 32rpx 0; + border-bottom: 1rpx solid rgba(0, 0, 0, 0.05); + transition: all 0.3s; + } + + .zhuanqu-item:active { + background: rgba(255, 204, 0, 0.05); + } + + .zhuanqu-item-content { + display: flex; + flex-direction: column; + align-items: center; + padding: 0 20rpx; + } + + .zhuanqu-name { + font-size: 26rpx; + color: #444; /* 颜色加深 */ + text-align: center; + line-height: 1.4; + margin-bottom: 8rpx; + transition: all 0.3s; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + font-weight: 500; + } + + .zhuanqu-active .zhuanqu-name { + color: #ff9900; + font-weight: 600; + text-shadow: 0 2rpx 4rpx rgba(255, 153, 0, 0.2); + } + + .zhuanqu-count { + font-size: 22rpx; + color: #888; + transition: all 0.3s; + } + + .zhuanqu-active .zhuanqu-count { + color: #ffaa00; + font-weight: 500; + } + + .zhuanqu-active-indicator { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + width: 6rpx; + height: 44rpx; + background: linear-gradient(180deg, #ffcc00, #ffaa00); + border-radius: 3rpx 0 0 3rpx; + opacity: 0; + transition: all 0.3s; + overflow: hidden; + } + + .zhuanqu-indicator-glow { + opacity: 1; + animation: indicatorGlow 2s infinite; + box-shadow: 0 0 20rpx rgba(255, 204, 0, 0.5); + } + + .indicator-sparkle { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 10rpx; + background: rgba(255, 255, 255, 0.6); + animation: sparkleMove 1.5s infinite; + } + + .zhuanqu-hover-glow { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255, 204, 0, 0.05), transparent); + opacity: 0; + transition: opacity 0.3s; + pointer-events: none; + } + + .zhuanqu-item:active .zhuanqu-hover-glow { + opacity: 1; + } + + .zhuanqu-click-effect { + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 204, 0, 0.1); + transform: translate(-50%, -50%); + opacity: 0; + pointer-events: none; + } + + .zhuanqu-item:active .zhuanqu-click-effect { + animation: rippleEffect 0.6s ease-out; + } + + .zhuanqu-empty { + display: flex; + flex-direction: column; + align-items: center; + padding: 120rpx 0; + } + + .zhuanqu-empty-icon { + width: 120rpx; + height: 120rpx; + opacity: 0.2; + margin-bottom: 28rpx; + } + + .zhuanqu-empty-text { + font-size: 26rpx; + color: #999; + } + + /* 右边:商品列表(紧凑布局) */ + .shangpin-right { + flex: 1; + background: #fafafa; + } + + .shangpin-list { + padding: 0; + } + + .shangpin-item { + background: white; + padding: 28rpx 30rpx 22rpx; /* 底部距离缩小 */ + position: relative; + display: flex; + align-items: flex-start; + min-height: 140rpx; + overflow: hidden; + } + + .shangpin-item:active { + background: rgba(0, 0, 0, 0.02); + } + + .shangpin-image-box { + margin-right: 24rpx; + flex-shrink: 0; + position: relative; + } + + .shangpin-image { + width: 120rpx; + height: 120rpx; + border-radius: 14rpx; + background: #f5f5f5; + object-fit: cover; + } + + .shangpin-image-glow { + position: absolute; + top: -4rpx; + left: -4rpx; + right: -4rpx; + bottom: -4rpx; + border-radius: 18rpx; + background: linear-gradient(135deg, rgba(255, 204, 0, 0.2), transparent); + opacity: 0; + transition: opacity 0.3s; + pointer-events: none; + } + + .shangpin-item:active .shangpin-image-glow { + opacity: 0.5; + } + + .image-corner { + position: absolute; + top: 0; + right: 0; + width: 20rpx; + height: 20rpx; + background: linear-gradient(135deg, #ffcc00, transparent 70%); + border-radius: 0 14rpx 0 0; + } + + .shangpin-info { + flex: 1; + display: flex; + flex-direction: column; + height: 120rpx; + justify-content: space-between; + padding-top: 2rpx; /* 标题与图片顶部对齐 */ + } + + .shangpin-title { + margin-bottom: 0; + position: relative; + } + + .shangpin-title-text { + font-size: 30rpx; + font-weight: 500; + color: #222; /* 黑色加深 */ + line-height: 1.4; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + overflow: hidden; + } + + .title-underline { + position: absolute; + bottom: -4rpx; + left: 0; + width: 0; + height: 2rpx; + background: linear-gradient(90deg, #ffcc00, #ffaa00); + transition: width 0.3s; + } + + .shangpin-item:active .title-underline { + width: 40rpx; + } + + .shangpin-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: auto; + } + + .shangpin-price { + display: flex; + align-items: baseline; + position: relative; + } + + .price-icon { + font-size: 24rpx; + color: #ff6600; + font-weight: 500; + } + + .price-integer { + font-size: 36rpx; /* 适当调整大小 */ + color: #ff6600; + font-weight: 600; + margin-left: 2rpx; + } + + .price-decimal { + font-size: 24rpx; + color: #ff6600; + font-weight: 500; + } + + .price-glow { + position: absolute; + top: 50%; + left: 50%; + width: 80rpx; + height: 80rpx; + background: radial-gradient(circle, rgba(255, 102, 0, 0.1) 0%, transparent 70%); + transform: translate(-50%, -50%); + opacity: 0; + pointer-events: none; + } + + .shangpin-item:active .price-glow { + animation: priceGlow 0.6s; + } + + .detail-btn { + position: relative; + padding: 12rpx 28rpx; + background: linear-gradient(135deg, #ffcc00, #ffaa00); + border-radius: 40rpx; + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + overflow: hidden; + box-shadow: 0 6rpx 20rpx rgba(255, 170, 0, 0.2); + display: flex; + align-items: center; + justify-content: center; + min-width: 120rpx; + } + + .detail-btn:active { + transform: scale(0.95); + box-shadow: 0 4rpx 15rpx rgba(255, 170, 0, 0.4); + } + + .detail-btn-text { + font-size: 24rpx; + color: white; + font-weight: 500; + position: relative; + z-index: 2; + line-height: 1; + } + + .btn-glow-effect { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent); + transform: translateX(-100%); + transition: transform 0.6s; + } + + .detail-btn:active .btn-glow-effect { + transform: translateX(100%); + } + + .btn-halo { + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + transform: translate(-50%, -50%); + opacity: 0; + pointer-events: none; + } + + .detail-btn:active .btn-halo { + animation: buttonHalo 0.6s ease-out; + } + + .btn-star { + position: absolute; + width: 6rpx; + height: 6rpx; + background: white; + border-radius: 50%; + opacity: 0; + pointer-events: none; + } + + .detail-btn:active .btn-star { + animation: starSparkle 0.8s ease-out; + } + + .item-divider { + position: absolute; + left: 30rpx; + right: 30rpx; + bottom: 0; + height: 1rpx; + background: linear-gradient(90deg, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.08)); + } + + .item-hover-glow { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, transparent, rgba(255, 204, 0, 0.03), transparent); + opacity: 0; + transition: opacity 0.3s; + pointer-events: none; + } + + .shangpin-item:active .item-hover-glow { + opacity: 1; + } + + .item-ripple-effect { + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 204, 0, 0.1); + transform: translate(-50%, -50%); + opacity: 0; + pointer-events: none; + } + + .shangpin-item:active .item-ripple-effect { + animation: rippleEffect 0.6s ease-out; + } + + .shangpin-empty { + display: flex; + flex-direction: column; + align-items: center; + padding: 150rpx 0; + } + + .shangpin-empty-icon { + width: 180rpx; + height: 180rpx; + opacity: 0.15; + margin-bottom: 36rpx; + } + + .shangpin-empty-text { + font-size: 28rpx; + color: #999; + margin-bottom: 40rpx; + } + + .shangpin-empty-btn { + background: linear-gradient(135deg, #ffcc00, #ffaa00); + color: white; + font-size: 26rpx; + font-weight: 500; + padding: 16rpx 44rpx; + border-radius: 40rpx; + box-shadow: 0 8rpx 24rpx rgba(255, 170, 0, 0.3); + transition: all 0.3s; + } + + .shangpin-empty-btn:active { + transform: scale(0.95); + box-shadow: 0 4rpx 16rpx rgba(255, 170, 0, 0.4); + } + + .load-more { + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx 0; + } + + .load-more-line { + width: 60rpx; + height: 1rpx; + background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1), transparent); + } + + .load-more-text { + font-size: 24rpx; + color: #bbb; + margin: 0 20rpx; + } + + /* 搜索结果弹窗 */ + .search-results-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 2000; + display: flex; + } + + .search-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(5rpx); + } + + .search-results-content { + position: absolute; + top: 100rpx; + left: 32rpx; + right: 32rpx; + bottom: 100rpx; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(30rpx); + border-radius: 24rpx; + box-shadow: + 0 20rpx 60rpx rgba(0, 0, 0, 0.2), + inset 0 1rpx 0 rgba(255, 255, 255, 0.8); + border: 1rpx solid rgba(255, 255, 255, 0.3); + display: flex; + flex-direction: column; + overflow: hidden; + } + + .search-results-header { + padding: 32rpx; + border-bottom: 1rpx solid rgba(0, 0, 0, 0.06); + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; + } + + .search-header-left { + display: flex; + align-items: baseline; + } + + .search-results-title { + font-size: 34rpx; + font-weight: 600; + color: #333; + } + + .search-results-count { + font-size: 24rpx; + color: #999; + margin-left: 12rpx; + } + + .search-close { + width: 44rpx; + height: 44rpx; + border-radius: 50%; + background: rgba(0, 0, 0, 0.05); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s; + } + + .search-close:active { + background: rgba(0, 0, 0, 0.1); + transform: scale(0.9); + } + + .close-icon { + width: 20rpx; + height: 20rpx; + opacity: 0.6; + } + + .search-results-list { + flex: 1; + height: 100%; /* 新增:修复滚动问题的关键代码 */ + } + + .search-result-item { + display: flex; + padding: 28rpx 32rpx; + border-bottom: 1rpx solid rgba(0, 0, 0, 0.04); + position: relative; + transition: all 0.3s; + } + + .search-result-item:active { + background: rgba(0, 0, 0, 0.02); + transform: translateY(-2rpx); + } + + .search-result-left { + margin-right: 24rpx; + position: relative; + } + + .search-result-image { + width: 120rpx; + height: 120rpx; + border-radius: 16rpx; + background: #f5f5f5; + object-fit: cover; + } + + .search-image-glow { + position: absolute; + top: -4rpx; + left: -4rpx; + right: -4rpx; + bottom: -4rpx; + border-radius: 20rpx; + background: linear-gradient(135deg, rgba(255, 204, 0, 0.2), transparent); + opacity: 0; + transition: opacity 0.3s; + pointer-events: none; + } + + .search-result-item:active .search-image-glow { + opacity: 0.5; + } + + .search-result-right { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + } + + .search-result-title { + font-size: 28rpx; + font-weight: 500; + color: #333; + line-height: 1.4; + margin-bottom: 16rpx; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + } + + .search-result-info { + display: flex; + gap: 12rpx; + margin-bottom: 20rpx; + } + + .info-tag { + padding: 6rpx 12rpx; + background: rgba(255, 204, 0, 0.1); + border-radius: 6rpx; + } + + .search-result-category { + font-size: 22rpx; + color: #666; + } + + .search-result-zhuanqu { + font-size: 22rpx; + color: #888; + } + + .search-result-price { + display: flex; + align-items: baseline; + } + + .search-item-glow { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, transparent, rgba(255, 204, 0, 0.05), transparent); + opacity: 0; + transition: opacity 0.3s; + pointer-events: none; + } + + .search-result-item:active .search-item-glow { + opacity: 1; + } + + .search-empty { + display: flex; + flex-direction: column; + align-items: center; + padding: 160rpx 0; + } + + .search-empty-icon { + width: 160rpx; + height: 160rpx; + opacity: 0.2; + margin-bottom: 36rpx; + } + + .search-empty-text { + font-size: 30rpx; + color: #999; + margin-bottom: 16rpx; + } + + .search-empty-tip { + font-size: 24rpx; + color: #bbb; + } + + .search-loading { + display: flex; + flex-direction: column; + align-items: center; + padding: 160rpx 0; + } + + .search-spinner { + width: 60rpx; + height: 60rpx; + position: relative; + margin-bottom: 24rpx; + } + + .spinner-circle { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 4rpx solid transparent; + border-top-color: #ffcc00; + border-radius: 50%; + animation: spin 1s linear infinite; + } + + .search-loading-text { + font-size: 26rpx; + color: #999; + } + + /* 全局加载状态 */ + .loading-toast { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(10rpx); + display: flex; + align-items: center; + justify-content: center; + z-index: 3000; + } + + .loading-content { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(20rpx); + padding: 60rpx 80rpx; + border-radius: 24rpx; + display: flex; + flex-direction: column; + align-items: center; + position: relative; + border: 1rpx solid rgba(255, 255, 255, 0.1); + } + + .loading-spinner { + width: 80rpx; + height: 80rpx; + position: relative; + margin-bottom: 30rpx; + } + + .spinner-ring { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 4rpx solid rgba(255, 255, 255, 0.2); + border-radius: 50%; + } + + .spinner-dot { + position: absolute; + top: 0; + left: 50%; + width: 12rpx; + height: 12rpx; + background: #ffcc00; + border-radius: 50%; + transform: translateX(-50%); + animation: spin 1s linear infinite; + } + + .loading-text { + font-size: 28rpx; + color: white; + font-weight: 500; + } + + .loading-glow { + position: absolute; + top: 50%; + left: 50%; + width: 200rpx; + height: 200rpx; + background: radial-gradient(circle, rgba(255, 204, 0, 0.2) 0%, transparent 70%); + transform: translate(-50%, -50%); + z-index: -1; + } + + /* 动画定义 */ + @keyframes glowPulse { + 0%, 100% { + opacity: 0.4; + transform: scale(1); + } + 50% { + opacity: 0.8; + transform: scale(1.05); + } + } + + @keyframes sparkleFlash { + 0%, 100% { + opacity: 0; + transform: translate(-50%, -50%) scale(0); + } + 50% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + filter: blur(0); + } + } + + @keyframes borderGlow { + 0%, 100% { + border-color: rgba(255, 204, 0, 0.3); + } + 50% { + border-color: rgba(255, 204, 0, 0.8); + } + } + + @keyframes indicatorGlow { + 0%, 100% { + box-shadow: 0 0 20rpx rgba(255, 204, 0, 0.3); + } + 50% { + box-shadow: 0 0 30rpx rgba(255, 204, 0, 0.8); + } + } + + @keyframes sparkleMove { + 0% { + transform: translateY(-100%); + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + transform: translateY(400%); + opacity: 0; + } + } + + @keyframes rippleEffect { + 0% { + width: 0; + height: 0; + opacity: 0.5; + } + 100% { + width: 200rpx; + height: 200rpx; + opacity: 0; + } + } + + @keyframes priceGlow { + 0% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.5); + } + 50% { + opacity: 0.6; + transform: translate(-50%, -50%) scale(1); + } + 100% { + opacity: 0; + transform: translate(-50%, -50%) scale(1.5); + } + } + + @keyframes buttonHalo { + 0% { + width: 0; + height: 0; + opacity: 0.6; + } + 100% { + width: 200rpx; + height: 200rpx; + opacity: 0; + } + } + + @keyframes starSparkle { + 0% { + opacity: 0; + transform: translate(0, 0) scale(0); + } + 20% { + opacity: 1; + transform: translate(-40rpx, -30rpx) scale(1); + } + 40% { + opacity: 1; + transform: translate(30rpx, -20rpx) scale(1); + } + 60% { + opacity: 1; + transform: translate(-20rpx, 40rpx) scale(1); + } + 80% { + opacity: 1; + transform: translate(40rpx, 20rpx) scale(1); + } + 100% { + opacity: 0; + transform: translate(0, 0) scale(0); + } + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } \ No newline at end of file diff --git a/pages/guanshiduan/guanshiduan.js b/pages/guanshiduan/guanshiduan.js new file mode 100644 index 0000000..6b3283e --- /dev/null +++ b/pages/guanshiduan/guanshiduan.js @@ -0,0 +1,539 @@ +// pages/guanshizhongxin/guanshizhongxin.js +import request from '../../utils/request.js'; +import popupService from '../../services/popupService.js'; + +Page({ + data: { + // 图片路径对象(逻辑层拼接完整URL) + imgUrls: {}, + + isGuanshi: false, + isLoading: false, + inviteCode: '', + uid: '', + nicheng: '', + avatarUrl: '', + + // 管事信息 + gszhstatus: '', + yaoqingzongshu: 0, + fenyongzonge: '0.00', + fenyongtixian: '0.00', + yichongzhiDashou: 0, + zhuanhualv: 0, + + // 称号列表 + chenghaoList: [], + + // 邀请人(组长)缓存 + inviterCache: null, + }, + + onLoad(options) { + // 拼接所有图片完整路径 + const app = getApp(); + const ossImageUrl = app.globalData.ossImageUrl || ''; + const imgBase = ossImageUrl + 'beijing/guanshiduan/'; + this.setData({ + imgUrls: { + pageBg: imgBase + 'page-bg.jpg', + topDecor: imgBase + 'top-decor.png', + cardBg1: imgBase + 'card-bg1.png', + cardBg2: imgBase + 'card-bg2.png', + iconRefresh: imgBase + 'icon-refresh.png', + iconCopy: imgBase + 'icon-copy.png', + avatarDefault: ossImageUrl + 'avatar/default.jpg', + iconInvite: imgBase + 'icon-invite.png', + iconSub: imgBase + 'icon-sub.png', + iconRecord: imgBase + 'icon-record.png', + iconRank: imgBase + 'icon-rank.png', + iconPoster: imgBase + 'icon-poster.png', + iconWithdraw: imgBase + 'icon-withdraw.png', + iconContact: imgBase + 'icon-contact.png', + iconArrow: imgBase + 'icon-arrow.png', + } + }); + + // 处理扫码 scene + if (options.scene) { + try { + const scene = decodeURIComponent(options.scene); + options.inviteCode = scene; + } catch (e) { + console.error('scene解码失败', e); + } + } + + this.checkGuanshiStatus(); + + const { inviteCode } = options || {}; + if (inviteCode) { + this.setData({ inviteCode }); + setTimeout(() => { + this.handleInviteCodeWithLoginCheck(inviteCode); + }, 500); + } + + if (!app.globalData.hasShownPopupOnColdStart) { + app.globalData.hasShownPopupOnColdStart = true; + setTimeout(() => { + popupService.checkAndShow(this, 'guanshiduan'); + }, 300); + } + }, + + onHide() { + const popupComp = this.selectComponent('#popupNotice'); + if (popupComp && popupComp.cleanup) { + popupComp.cleanup(); + } + }, + + onShow() { + const inviterCache = wx.getStorageSync('inviterCache') || null; + this.setData({ inviterCache }); + + this.registerNotificationComponent(); + + if (this.data.isGuanshi) { + setTimeout(() => { + this.getGuanshiInfo(); + this.fetchChenghaoList(); + }, 300); + } + + const pages = getCurrentPages(); + const currentPage = pages[pages.length - 1]; + const options = currentPage.options || {}; + if (options.inviteCode && !this.data.isGuanshi) { + setTimeout(() => { + this.handleInviteCodeWithLoginCheck(options.inviteCode); + }, 300); + } + }, + + // ========== 全局通知注册 ========== + registerNotificationComponent() { + const app = getApp(); + const comp = this.selectComponent('#global-notification'); + if (comp && comp.showNotification) { + app.globalData.globalNotification = { + show: (data) => comp.showNotification(data), + hide: () => comp.hideNotification() + }; + } + }, + + // ========== 称号列表(新接口,区分管事) ========== + async fetchChenghaoList() { + try { + const res = await request({ + url: '/dengji/chenghao', + method: 'POST', + data: { role: 'guanshi' } + }); + if (res && res.data.code === 0 && res.data.data) { + this.setData({ chenghaoList: res.data.data.chenghao_list || [] }); + } + } catch (e) { + console.error('获取称号失败', e); + } + }, + + // ========== 联系组长(新接口) ========== + // 管事联系组长 —— 获取组长信息并跳转聊天 + // 管事联系组长 +async contactInviter() { + wx.showLoading({ title: '获取组长信息...', mask: true }); + try { + const res = await request({ + url: '/yonghu/guanshi_contact', + method: 'POST' + }); + + if (res && res.data.code === 200 && res.data.data) { + const inviter = res.data.data; + const ossImageUrl = getApp().globalData.ossImageUrl || ''; + const fullAvatar = inviter.touxiang + ? (inviter.touxiang.startsWith('http') ? inviter.touxiang : ossImageUrl + inviter.touxiang) + : ''; + + // 缓存邀请人信息 + const cacheData = { + uid: inviter.uid, + nicheng: inviter.nicheng || '', + avatar: fullAvatar + }; + wx.setStorageSync('inviterCache', cacheData); + this.setData({ inviterCache: cacheData }); + + // 跳转聊天(组长前缀请确认,默认 'Zz') + const param = { + toUserId: 'Zz' + inviter.uid, + toName: inviter.nicheng || '组长', + toAvatar: fullAvatar + }; + wx.navigateTo({ + url: '/pages/liaotian/liaotian?data=' + encodeURIComponent(JSON.stringify(param)) + }); + } else { + wx.showToast({ title: '未绑定邀请人', icon: 'none' }); + } + } catch (e) { + wx.showToast({ title: '网络错误', icon: 'none' }); + } finally { + wx.hideLoading(); + } +}, + // ========== 邀请码处理(完全照搬打手端) ========== + handleInviteCodeWithLoginCheck(inviteCode) { + const token = wx.getStorageSync('token'); + const uid = wx.getStorageSync('uid'); + if (!token || !uid) { + this.loginAndRegisterWithInviteCode(inviteCode); + } else { + if (!this.data.isGuanshi) { + wx.showLoading({ title: '自动注册中...', mask: true }); + this.callRegisterApi(inviteCode); + } else { + wx.showToast({ title: '您已是管事', icon: 'success' }); + } + } + }, + + loginAndRegisterWithInviteCode(inviteCode) { + const that = this; + wx.showLoading({ title: '登录注册中...', mask: true }); + wx.login({ + success: (loginRes) => { + if (loginRes.code) { + that.callLoginRegisterApi(loginRes.code, inviteCode); + } else { + wx.hideLoading(); + wx.showToast({ title: '微信登录失败', icon: 'none' }); + } + }, + fail: () => { + wx.hideLoading(); + wx.reLaunch({ url: '/pages/gerenzhongxin/gerenzhongxin' }); + } + }); + }, + + callLoginRegisterApi(code, inviteCode) { + const that = this; + const app = getApp(); + const apiBaseUrl = app.globalData.apiBaseUrl; + if (!apiBaseUrl) { + wx.hideLoading(); + wx.showToast({ title: '服务器配置错误', icon: 'none' }); + return; + } + wx.request({ + url: apiBaseUrl + '/yonghu/zuzhangyqmzc', + method: 'POST', + data: { code: code, inviteCode: inviteCode }, + header: { 'content-type': 'application/json' }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data) { + const userData = res.data.data; + if (userData.token) wx.setStorageSync('token', userData.token); + const identityFields = ['nicheng', 'uid', 'touxiang', 'dashoustatus', 'guanshistatus', 'shangjiastatus', 'zuzhangstatus']; + identityFields.forEach(field => { + if (userData[field] !== undefined) { + wx.setStorageSync(field, userData[field]); + app.globalData[field] = userData[field]; + } + }); + if (userData.guanshistatus === undefined) { + wx.setStorageSync('guanshistatus', 1); + app.globalData.guanshistatus = 1; + } + const groupFields = ['dashouqun', 'dashouqunid', 'guanshiqun', 'guanshiqunid']; + groupFields.forEach(field => { + if (userData[field] !== undefined) { + wx.setStorageSync(field, userData[field]); + app.globalData[field] = userData[field]; + } + }); + const guanshiData = { + gszhstatus: userData.gszhstatus || '', + yaoqingzongshu: userData.yaoqingzongshu || 0, + fenyongzonge: userData.fenyongzonge || '0.00', + fenyongtixian: userData.fenyongtixian || '0.00', + yichongzhiDashou: userData.yichongzhiDashou || 0 + }; + if (!app.globalData.guanshi) app.globalData.guanshi = {}; + Object.assign(app.globalData.guanshi, guanshiData); + const zhuanhualv = guanshiData.yaoqingzongshu > 0 + ? (guanshiData.yichongzhiDashou / guanshiData.yaoqingzongshu * 100).toFixed(1) + : 0; + that.setData({ + isGuanshi: true, + uid: userData.uid || '', + nicheng: userData.nicheng || '', + ...guanshiData, + zhuanhualv, + isLoading: false + }); + that.loadUserInfo(); + that.switchToGuanshiRole(); + wx.hideLoading(); + wx.showToast({ title: '注册成功!', icon: 'success', duration: 2000 }); + } else { + wx.hideLoading(); + wx.showToast({ title: (res.data && res.data.msg) || '注册失败', icon: 'none' }); + } + }, + fail: () => { + wx.hideLoading(); + wx.showToast({ title: '网络请求失败', icon: 'none' }); + } + }); + }, + + async callRegisterApi(inviteCode) { + try { + const res = await request({ + url: '/yonghu/guanshizhuce', + method: 'POST', + data: { inviteCode } + }); + if (res && res.data.code === 200 && res.data.data) { + const userData = res.data.data; + const identityFields = ['nicheng', 'uid', 'touxiang', 'dashoustatus', 'guanshistatus', 'shangjiastatus', 'zuzhangstatus']; + const app = getApp(); + identityFields.forEach(field => { + if (userData[field] !== undefined) { + wx.setStorageSync(field, userData[field]); + app.globalData[field] = userData[field]; + } + }); + if (userData.guanshistatus === undefined) { + wx.setStorageSync('guanshistatus', 1); + app.globalData.guanshistatus = 1; + } + const guanshiData = { + gszhstatus: userData.gszhstatus || '', + yaoqingzongshu: userData.yaoqingzongshu || 0, + fenyongzonge: userData.fenyongzonge || '0.00', + fenyongtixian: userData.fenyongtixian || '0.00', + yichongzhiDashou: userData.yichongzhiDashou || 0 + }; + if (!app.globalData.guanshi) app.globalData.guanshi = {}; + Object.assign(app.globalData.guanshi, guanshiData); + const zhuanhualv = guanshiData.yaoqingzongshu > 0 + ? (guanshiData.yichongzhiDashou / guanshiData.yaoqingzongshu * 100).toFixed(1) + : 0; + this.setData({ + isGuanshi: true, + uid: userData.uid || '', + nicheng: userData.nicheng || '', + ...guanshiData, + zhuanhualv, + isLoading: false + }); + this.loadUserInfo(); + this.switchToGuanshiRole(); + wx.hideLoading(); + wx.showToast({ title: '注册成功!', icon: 'success', duration: 2000 }); + } else { + wx.hideLoading(); + wx.showToast({ title: res?.data?.msg || '注册失败', icon: 'none' }); + } + } catch (e) { + console.error('注册失败:', e); + wx.hideLoading(); + wx.showToast({ title: e.message || '注册失败', icon: 'error' }); + } + }, + + switchToGuanshiRole() { + const app = getApp(); + app.globalData.currentRole = 'guanshi'; + wx.setStorageSync('currentRole', 'guanshi'); + const tabBar = this.getTabBar(); + if (tabBar && tabBar.refresh) tabBar.refresh(); + if (app.connectForCurrentRole) { + app.connectForCurrentRole(); + } else if (app.ensureConnection) { + app.ensureConnection(); + } + }, + + checkGuanshiStatus() { + try { + const status = wx.getStorageSync('guanshistatus'); + if (status === 1) { + this.setData({ isGuanshi: true }); + this.loadUserInfo(); + this.checkGlobalData(); + } else { + this.setData({ isGuanshi: false }); + } + } catch (e) { + this.setData({ isGuanshi: false }); + } + }, + + loadUserInfo() { + const app = getApp(); + const globalData = app.globalData || {}; + const touxiang = wx.getStorageSync('touxiang'); + const ossImageUrl = globalData.ossImageUrl || ''; + const morentouxiang = 'avatar/default.jpg'; + let avatarUrl = ''; + if (touxiang) { + avatarUrl = ossImageUrl + (touxiang.startsWith('/') ? touxiang : '/' + touxiang); + } else { + avatarUrl = ossImageUrl + (morentouxiang.startsWith('/') ? morentouxiang : '/' + morentouxiang); + } + this.setData({ + avatarUrl, + uid: wx.getStorageSync('uid') || '', + nicheng: globalData.nicheng || '' + }); + }, + + checkGlobalData() { + const app = getApp(); + const gd = app.globalData.guanshi || {}; + const needRefresh = !gd.yaoqingzongshu && !gd.fenyongzonge && !gd.fenyongtixian; + if (needRefresh) { + this.getGuanshiInfo(); + } else { + this.setDataFromGlobalData(); + } + }, + + setDataFromGlobalData() { + const gd = getApp().globalData.guanshi || {}; + const zhuanhualv = gd.yaoqingzongshu > 0 + ? (gd.yichongzhiDashou / gd.yaoqingzongshu * 100).toFixed(1) + : 0; + this.setData({ + gszhstatus: gd.gszhstatus || '', + yaoqingzongshu: gd.yaoqingzongshu || 0, + fenyongzonge: gd.fenyongzonge || '0.00', + fenyongtixian: gd.fenyongtixian || '0.00', + yichongzhiDashou: gd.yichongzhiDashou || 0, + zhuanhualv, + isLoading: false + }); + }, + + async getGuanshiInfo() { + this.setData({ isLoading: true }); + try { + const res = await request({ + url: '/yonghu/guanshixinxi', + method: 'POST' + }); + if (res && res.data.code === 200) { + const data = res.data.data; + const app = getApp(); + if (!app.globalData.guanshi) app.globalData.guanshi = {}; + const updateData = { + gszhstatus: data.gszhstatus || '', + yaoqingzongshu: data.yaoqingzongshu || 0, + fenyongzonge: data.fenyongzonge || '0.00', + fenyongtixian: data.fenyongtixian || '0.00', + yichongzhiDashou: data.yichongzhiDashou || 0 + }; + Object.assign(app.globalData.guanshi, updateData); + const zhuanhualv = updateData.yaoqingzongshu > 0 + ? (updateData.yichongzhiDashou / updateData.yaoqingzongshu * 100).toFixed(1) + : 0; + this.setData({ + ...updateData, + zhuanhualv, + isLoading: false + }); + wx.showToast({ title: '信息已更新', icon: 'success', duration: 1500 }); + } + } catch (e) { + this.setData({ isLoading: false }); + wx.showToast({ title: e.message || '获取失败', icon: 'error' }); + } + }, + + refreshGuanshiInfo() { + this.getGuanshiInfo(); + this.fetchChenghaoList(); + }, + + onInviteCodeInput(e) { + this.setData({ inviteCode: e.detail.value.trim() }); + }, + + async onRegister() { + const { inviteCode } = this.data; + if (!inviteCode) { + wx.showToast({ title: '请输入邀请码', icon: 'none' }); + return; + } + if (inviteCode.length > 100) { + wx.showToast({ title: '邀请码过长', icon: 'none' }); + return; + } + this.setData({ isLoading: true }); + try { + const res = await request({ + url: '/yonghu/guanshizhuce', + method: 'POST', + data: { inviteCode } + }); + if (res && res.data.code === 200) { + const userData = res.data.data; + const identityFields = ['dashoustatus', 'guanshistatus', 'shangjiastatus', 'zuzhangstatus']; + const app = getApp(); + identityFields.forEach(field => { + if (userData[field] !== undefined) { + wx.setStorageSync(field, userData[field]); + app.globalData[field] = userData[field]; + } + }); + if (userData.guanshistatus === undefined) { + wx.setStorageSync('guanshistatus', 1); + app.globalData.guanshistatus = 1; + } + if (!app.globalData.guanshi) app.globalData.guanshi = {}; + const updateData = { + gszhstatus: userData.gszhstatus || '', + yaoqingzongshu: userData.yaoqingzongshu || 0, + fenyongzonge: userData.fenyongzonge || '0.00', + fenyongtixian: userData.fenyongtixian || '0.00', + yichongzhiDashou: userData.yichongzhiDashou || 0 + }; + Object.assign(app.globalData.guanshi, updateData); + this.setData({ + isGuanshi: true, + ...updateData, + isLoading: false + }); + this.loadUserInfo(); + wx.showToast({ title: '注册成功!', icon: 'success', duration: 2000 }); + } + } catch (e) { + this.setData({ isLoading: false }); + wx.showToast({ title: e.message || '注册失败', icon: 'error' }); + } + }, + + copyUid() { + const { uid } = this.data; + if (!uid) return; + wx.setClipboardData({ + data: uid, + success: () => wx.showToast({ title: '已复制', icon: 'success' }) + }); + }, + + // ========== 页面跳转 ========== + goToWithdraw() { wx.navigateTo({ url: '/pages/tixian/tixian' }); }, + goToInviteUser() { wx.navigateTo({ url: '/pages/yaoqingdashou/yaoqingdashou' }); }, + goToMySubordinate() { wx.navigateTo({ url: '/pages/wodedashou/wodedashou' }); }, + goToRechargeRecord() { wx.navigateTo({ url: '/pages/czjilu/czjilu' }); }, + goToGuanshiRanking() { wx.navigateTo({ url: '/pages/guanshipaihang/guanshipaihang' }); }, + goToPromoPoster() { wx.navigateTo({ url: '/pages/haibao/haibao' }); } +}); \ No newline at end of file diff --git a/pages/guanshiduan/guanshiduan.json b/pages/guanshiduan/guanshiduan.json new file mode 100644 index 0000000..9aca2d3 --- /dev/null +++ b/pages/guanshiduan/guanshiduan.json @@ -0,0 +1,11 @@ +{ + "usingComponents": { + "global-notification": "/components/global-notification/global-notification", + "chenghao-tag": "/components/chenghao-tag/chenghao-tag" + }, + "navigationBarBackgroundColor": "#f0f4f8", + "navigationBarTextStyle": "black", + "navigationBarTitleText": "管事中心", + "backgroundColor": "#f0f4f8", + "enablePullDownRefresh": false + } \ No newline at end of file diff --git a/pages/guanshiduan/guanshiduan.wxml b/pages/guanshiduan/guanshiduan.wxml new file mode 100644 index 0000000..3ba8dbc --- /dev/null +++ b/pages/guanshiduan/guanshiduan.wxml @@ -0,0 +1,122 @@ + + + + + + + ✦ 管事认证 ✦ + + + 立即注册 + + + + + + + + + + + + + + + + + + + + + + + + + + + 可提现分成 + + {{fenyongtixian}} + + + + + + {{yaoqingzongshu}} + 邀请总数 + + + {{yichongzhiDashou}} + 已充值 + + + {{fenyongzonge}} + 分成总额 + + + {{zhuanhualv}}% + 有效率 + + + + + + 提现 + + + + + + + + 邀请用户 + + + + 我的下级 + + + + 会员记录 + + + + 管事排行 + + + + 推广海报 + + + + 联系组长 + + + + + + + + + {{inviterCache.nicheng || '联系组长'}} + ID: {{inviterCache.uid}} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pages/guanshiduan/guanshiduan.wxss b/pages/guanshiduan/guanshiduan.wxss new file mode 100644 index 0000000..3cf8113 --- /dev/null +++ b/pages/guanshiduan/guanshiduan.wxss @@ -0,0 +1,367 @@ +/* ========== 全局背景 ========== */ +page { + background: #F8FAFE; + font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif; + color: #1A1A1A; + } + + /* 背景图清晰可见 */ + .page-bg { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.4; + z-index: 0; + pointer-events: none; + } + + /* ========== 未注册区域 ========== */ + .unreg-area { + position: relative; + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 0 50rpx; + } + + .unreg-card { + width: 100%; + background: rgba(255, 255, 255, 0.75); + backdrop-filter: blur(25rpx); + -webkit-backdrop-filter: blur(25rpx); + border-radius: 48rpx; + padding: 60rpx 50rpx; + text-align: center; + box-shadow: 0 20rpx 50rpx rgba(0,0,0,0.06); + border: 1rpx solid rgba(255,255,255,0.5); + } + + .unreg-title { + font-size: 40rpx; + font-weight: 700; + color: #1A1A1A; + display: block; + margin-bottom: 50rpx; + } + + .unreg-input { + width: 100%; + height: 96rpx; + background: #FFFFFF; + border-radius: 48rpx; + padding: 0 40rpx; + font-size: 30rpx; + color: #1A1A1A; + margin-bottom: 40rpx; + box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.03); + } + + .ph { + color: #999999; + } + + .unreg-btn { + width: 100%; + height: 96rpx; + background: rgba(255,255,255,0.6); + backdrop-filter: blur(15rpx); + border-radius: 48rpx; + display: flex; + align-items: center; + justify-content: center; + border: 2rpx solid rgba(0,0,0,0.08); + box-shadow: 0 12rpx 30rpx rgba(0,0,0,0.05); + } + .unreg-btn:active { opacity: 0.8; } + .unreg-btn-text { + color: #1A1A1A; + font-size: 34rpx; + font-weight: 600; + } + + /* ========== 已注册主区域 ========== */ + .main-body { + position: relative; + z-index: 2; + padding: 0 30rpx 40rpx; + } + + /* 刷新按钮 */ + .refresh-btn { + position: absolute; + top: 40rpx; + right: 40rpx; + width: 68rpx; + height: 68rpx; + background: rgba(255,255,255,0.75); + backdrop-filter: blur(12rpx); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + z-index: 20; + border: 1rpx solid rgba(255,255,255,0.6); + box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.06); + } + .refresh-icon { + width: 38rpx; + height: 38rpx; + } + + /* 用户卡片 */ + .user-card { + margin-top: 60rpx; + margin-bottom: 30rpx; + background: rgba(255,255,255,0.7); + backdrop-filter: blur(20rpx); + -webkit-backdrop-filter: blur(20rpx); + border-radius: 36rpx; + padding: 28rpx 30rpx; + display: flex; + align-items: center; + border: 1rpx solid rgba(255,255,255,0.5); + box-shadow: 0 12rpx 35rpx rgba(0,0,0,0.05); + } + + .user-avatar { + width: 100rpx; + height: 100rpx; + border-radius: 50%; + border: 3rpx solid #FFFFFF; + margin-right: 24rpx; + } + + .user-name { + font-size: 38rpx; + font-weight: 700; + color: #1A1A1A; + display: block; + margin-bottom: 8rpx; + } + + .uid-row { + display: inline-flex; + align-items: center; + background: rgba(0,0,0,0.03); + border-radius: 20rpx; + padding: 6rpx 18rpx; + } + + .uid-text { + font-size: 26rpx; + color: #1A1A1A; + margin-right: 10rpx; + } + + .copy-icon { + width: 28rpx; + height: 28rpx; + } + + /* 称号区域 */ + .badge-zone { + margin-bottom: 24rpx; + } + .badge-scroll { + white-space: nowrap; + } + + /* 资产卡片(纯玻璃质感) */ + .asset-card { + background: rgba(255,255,255,0.7); + backdrop-filter: blur(20rpx); + -webkit-backdrop-filter: blur(20rpx); + border-radius: 36rpx; + padding: 28rpx 30rpx; + margin-bottom: 28rpx; + border: 1rpx solid rgba(255,255,255,0.5); + box-shadow: 0 12rpx 35rpx rgba(0,0,0,0.05); + } + + .asset-title { + font-size: 26rpx; + color: #1A1A1A; + font-weight: 500; + display: block; + } + + .asset-amount { + display: flex; + align-items: baseline; + margin: 10rpx 0 22rpx; + } + + .amount-number { + font-size: 74rpx; + font-weight: 800; + color: #1A1A1A; + margin-right: 8rpx; + } + + .amount-unit { + font-size: 32rpx; + color: #1A1A1A; + font-weight: 600; + } + + /* 四列数据统计 */ + .four-stats { + display: flex; + align-items: center; + justify-content: space-around; + padding: 20rpx 0; + border-top: 1rpx solid rgba(0,0,0,0.06); + border-bottom: 1rpx solid rgba(0,0,0,0.06); + margin-bottom: 22rpx; + } + + .stat-item { + text-align: center; + flex: 1; + } + + .stat-val { + font-size: 28rpx; + font-weight: 700; + color: #1A1A1A; + display: block; + margin-bottom: 6rpx; + } + + .stat-label { + font-size: 20rpx; + color: #1A1A1A; + font-weight: 500; + } + + /* 提现按钮 */ + .withdraw-btn { + display: flex; + align-items: center; + justify-content: center; + background: rgba(255,255,255,0.55); + backdrop-filter: blur(12rpx); + border-radius: 48rpx; + padding: 16rpx 0; + border: 2rpx solid rgba(0,0,0,0.06); + box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.04); + } + + .withdraw-icon { + width: 36rpx; + height: 36rpx; + margin-right: 12rpx; + } + + .withdraw-text { + font-size: 30rpx; + font-weight: 600; + color: #1A1A1A; + } + + /* 功能入口三列布局 */ + .func-grid { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + margin-bottom: 28rpx; + } + + .func-item { + width: 31%; + aspect-ratio: 1 / 1; + background: rgba(255,255,255,0.7); + backdrop-filter: blur(16rpx); + -webkit-backdrop-filter: blur(16rpx); + border-radius: 28rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-bottom: 20rpx; + border: 1rpx solid rgba(255,255,255,0.5); + box-shadow: 0 8rpx 22rpx rgba(0,0,0,0.04); + } + + .func-icon { + width: 56rpx; + height: 56rpx; + margin-bottom: 12rpx; + } + + .func-label { + font-size: 26rpx; + font-weight: 500; + color: #1A1A1A; + text-align: center; + } + + /* 联系组长卡片(底部横条) */ + .contact-card { + display: flex; + align-items: center; + justify-content: space-between; + background: rgba(255,255,255,0.7); + backdrop-filter: blur(20rpx); + -webkit-backdrop-filter: blur(20rpx); + border-radius: 36rpx; + padding: 20rpx 30rpx; + margin-bottom: 40rpx; + border: 1rpx solid rgba(255,255,255,0.5); + box-shadow: 0 10rpx 25rpx rgba(0,0,0,0.04); + } + + .contact-left { + display: flex; + align-items: center; + flex: 1; + } + + .contact-avatar { + width: 60rpx; + height: 60rpx; + border-radius: 50%; + margin-right: 18rpx; + border: 2rpx solid #FFFFFF; + } + + .contact-name { + font-size: 28rpx; + font-weight: 500; + color: #1A1A1A; + } + + .contact-uid { + font-size: 22rpx; + color: #1A1A1A; + margin-top: 4rpx; + } + + .contact-arrow { + width: 24rpx; + height: 24rpx; + } + + /* 加载遮罩 */ + .loading-mask { + position: fixed; + top:0; left:0; right:0; bottom:0; + background: rgba(255,255,255,0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + } + .loading-spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid rgba(0,0,0,0.1); + border-top-color: #1A1A1A; + border-radius: 50%; + animation: spin 0.8s linear infinite; + } + @keyframes spin { to { transform: rotate(360deg); } } \ No newline at end of file diff --git a/pages/guanshipaihang/guanshipaihang.js b/pages/guanshipaihang/guanshipaihang.js new file mode 100644 index 0000000..c3771f6 --- /dev/null +++ b/pages/guanshipaihang/guanshipaihang.js @@ -0,0 +1,393 @@ +// pages/guanshi-paihang/guanshi-paihang.js +const app = getApp() +import request from '../../utils/request.js' + +Page({ + data: { + // 页面加载状态 + jiazaiZhuangtai: true, + + // 排行榜类型:1=今日邀请,2=今月邀请 + paihangLeixing: 1, + + // 榜单数据 + paihangShuju: [], + + // 是否展示真实数据(后端控制) + xianshiZhenshiShuju: false, + + // 虚拟数据(用于当后端不允许展示时) + xuniShuju: [], + + // 按钮防抖控制 + btnFangdou: false, + + // OSS图片基础URL(从全局变量获取) + ossImageUrl: '', + + // 默认头像(相对路径,需要与OSS基础URL拼接) + morentouxiang: '', + + // 用户类型:1=打手,2=管事,3=商家(当前页面固定为管事) + yonghuLeixing: 2, + + // 前三名特殊数据 + qiansanMing: [], + + // 动画相关 + crownAnimation: null, + + // 卡片点击特效 + flashCardIndex: -1, + flashCardType: '', + flashAnimation: null, + + // 切换按钮选中状态 + switchBgPosition: 0 + }, + + onLoad() { + // 设置页面标题 + wx.setNavigationBarTitle({ + title: '管事排行榜' + }) + + // 初始化全局变量 + this.setData({ + ossImageUrl: app.globalData.ossImageUrl || '', + morentouxiang: app.globalData.morentouxiang || '' + }) + + // 初始化虚拟数据 + this.initXuniShuju() + + // 加载排行榜数据 + this.huoquPaihangShuju() + + // 开始皇冠动画 + this.startCrownAnimation() + }, + + onShow() { + // 页面显示时恢复动画 + this.registerNotificationComponent(); + this.startCrownAnimation() + }, + registerNotificationComponent() { + const app = getApp(); + const notificationComp = this.selectComponent('#global-notification'); + + if (notificationComp && notificationComp.showNotification) { + console.log('🏪 商城页面注册通知组件'); + app.globalData.globalNotification = { + show: (data) => notificationComp.showNotification(data), + hide: () => notificationComp.hideNotification() + }; + } +}, + + onHide() { + // 页面隐藏时停止动画 + this.stopCrownAnimation() + }, + + onUnload() { + // 页面卸载时停止动画 + this.stopCrownAnimation() + }, + + // 初始化虚拟数据 + initXuniShuju() { + const xuniShuju = [] + const names = ['管事大神', '组织高手', '团队领袖', '精英管理', '运营达人', + '战略专家', '人才猎手', '组织核心', '管理大师', '招募高手'] + + for (let i = 0; i < 10; i++) { + xuniShuju.push({ + mingci: i + 1, + touxiang: '', + nicheng: names[i] || `虚拟管事${i + 1}`, + uid: `G${100000 + i}`, + jine: (88 - i * 7).toString(), // 管事显示的是数量,不是金额 + jineClass: 'normal' // 金额样式 + }) + } + this.setData({ xuniShuju }) + }, + + // 开始皇冠动画 + startCrownAnimation() { + this.stopCrownAnimation() + + const animation = wx.createAnimation({ + duration: 800, + timingFunction: 'ease-in-out' + }) + + let scale = 1 + const animate = () => { + scale = scale === 1 ? 1.15 : 1 + + animation.scale(scale).step({ + duration: 800, + timingFunction: 'ease-in-out' + }) + + this.setData({ + crownAnimation: animation.export() + }) + + this.data.crownAnimationTimer = setTimeout(() => { + animate() + }, 800) + } + + animate() + }, + + // 停止皇冠动画 + stopCrownAnimation() { + if (this.data.crownAnimationTimer) { + clearTimeout(this.data.crownAnimationTimer) + this.setData({ + crownAnimationTimer: null + }) + } + }, + + // 卡片点击特效 + handleCardTap(e) { + const index = e.currentTarget.dataset.index + const type = e.currentTarget.dataset.type // 'top3' 或 'normal' + + // 创建闪光动画 + const animation = wx.createAnimation({ + duration: 300, + timingFunction: 'ease-out' + }) + + // 快速闪光效果 + animation.opacity(0.9).step({ + duration: 50, + timingFunction: 'linear' + }) + + animation.opacity(1).step({ + duration: 250, + timingFunction: 'ease-out' + }) + + this.setData({ + flashCardIndex: index, + flashCardType: type, + flashAnimation: animation.export() + }) + + // 300ms后重置闪光状态 + setTimeout(() => { + this.setData({ + flashCardIndex: -1, + flashCardType: '' + }) + }, 300) + }, + + // 获取排行榜数据 + async huoquPaihangShuju() { + this.setData({ + jiazaiZhuangtai: true + }) + + try { + // 准备请求参数 + const requestData = { + leixing: this.data.paihangLeixing, + yonghu_leixing: this.data.yonghuLeixing + } + + // 使用封装的request发送请求 + const res = await request({ + url: '/peizhi/dsph', + method: 'POST', + data: requestData, + header: { + 'content-type': 'application/json' + } + }) + + // 处理返回数据 + if (res && res.data && res.data.code === 0) { + const responseData = res.data.data + + // 检查后端是否允许展示 + if (responseData.xianshi && responseData.paihang_list && responseData.paihang_list.length > 0) { + // 处理头像URL拼接和金额样式 + const processedList = this.processData(responseData.paihang_list) + + // 提取前三名特殊展示 + const qiansanMing = processedList.slice(0, 3) + const qitaPaihang = processedList.slice(3) + + this.setData({ + paihangShuju: qitaPaihang, + qiansanMing: qiansanMing, + xianshiZhenshiShuju: true, + jiazaiZhuangtai: false + }) + } else { + // 后端不允许展示或数据不足,显示虚拟数据 + this.xianshiXuniShuju() + } + } else { + // 请求失败,显示虚拟数据 + const errorMsg = res && res.data ? (res.data.msg || '获取数据失败') : '网络请求失败' + + wx.showToast({ + title: errorMsg, + icon: 'none', + duration: 2000 + }) + + this.xianshiXuniShuju() + } + } catch (error) { + console.error('获取排行榜数据失败:', error) + + wx.showToast({ + title: '网络请求异常,请重试', + icon: 'none', + duration: 2000 + }) + + this.xianshiXuniShuju() + } + }, + + // 处理数据(头像拼接和金额样式) + processData(list) { + const { ossImageUrl, morentouxiang } = this.data + + return list.map(item => { + // 优先使用返回的头像URL + let finalTouxiang = item.touxiang + + // 如果返回的头像为空,使用默认头像 + if (!finalTouxiang && morentouxiang) { + finalTouxiang = morentouxiang + } + + // 拼接完整的URL + let fullTouxiangUrl = '' + if (finalTouxiang && ossImageUrl) { + // 确保路径不以斜杠开头 + let touxiangPath = finalTouxiang + if (touxiangPath.startsWith('/')) { + touxiangPath = touxiangPath.substring(1) + } + fullTouxiangUrl = ossImageUrl + touxiangPath + } + + // 处理数量样式(管事显示的是数量,不是金额) + const jine = parseInt(item.jine || 0) + let jineClass = 'normal' + const jineStr = jine.toString() // 管事显示整数数量 + + // 判断数量位数 + if (jineStr.length >= 5) { // 5位数及以上 + jineClass = 'very-small' + } else if (jineStr.length >= 4) { // 4位数 + jineClass = 'small' + } else if (jineStr.length >= 3) { // 3位数 + jineClass = 'medium' + } + + return { + ...item, + full_touxiang: fullTouxiangUrl, + jine: jineStr, + jineClass: jineClass + } + }) + }, + + // 显示虚拟数据 + xianshiXuniShuju() { + const processedList = this.processData(this.data.xuniShuju) + const qiansanMing = processedList.slice(0, 3) + const qitaPaihang = processedList.slice(3) + + this.setData({ + paihangShuju: qitaPaihang, + qiansanMing: qiansanMing, + xianshiZhenshiShuju: false, + jiazaiZhuangtai: false + }) + }, + + // 切换排行榜类型(今日/今月) + qiehuangPaihangLeixing(e) { + // 防抖处理:防止连续点击 + if (this.data.btnFangdou) return + + const leixing = e.currentTarget.dataset.leixing + if (leixing == this.data.paihangLeixing) return + + // 设置防抖状态 + this.setData({ + btnFangdou: true + }) + + // 更新切换按钮位置 + this.setData({ + paihangLeixing: leixing, + switchBgPosition: leixing == 1 ? 0 : 1 + }) + + // 重新加载数据 + this.huoquPaihangShuju() + + // 1秒后解除防抖 + setTimeout(() => { + this.setData({ + btnFangdou: false + }) + }, 1000) + }, + + // 用户下拉刷新 + onPullDownRefresh() { + this.huoquPaihangShuju() + wx.stopPullDownRefresh() + }, + + // 图片加载失败处理 + tupianJiazaiShibai(e) { + const index = e.currentTarget.dataset.index + const type = e.currentTarget.dataset.type // 'top3' 或 'normal' + + // 使用默认头像 + const { ossImageUrl, morentouxiang } = this.data + let defaultUrl = '' + if (morentouxiang && ossImageUrl) { + let defaultPath = morentouxiang + if (defaultPath.startsWith('/')) { + defaultPath = defaultPath.substring(1) + } + defaultUrl = ossImageUrl + defaultPath + } + + if (type === 'top3') { + // 前三名头像加载失败 + const key = `qiansanMing[${index}].full_touxiang` + this.setData({ + [key]: defaultUrl + }) + } else { + // 普通排名头像加载失败 + const key = `paihangShuju[${index}].full_touxiang` + this.setData({ + [key]: defaultUrl + }) + } + } +}) \ No newline at end of file diff --git a/pages/guanshipaihang/guanshipaihang.json b/pages/guanshipaihang/guanshipaihang.json new file mode 100644 index 0000000..f6237dd --- /dev/null +++ b/pages/guanshipaihang/guanshipaihang.json @@ -0,0 +1,12 @@ +{ + "usingComponents": { + "global-notification": "/components/global-notification/global-notification" + }, + "navigationBarTitleText": "管事排行榜", + "navigationBarBackgroundColor": "#000000", + "navigationBarTextStyle": "white", + "enablePullDownRefresh": false, + "backgroundColor": "#000000", + "backgroundTextStyle": "dark", + "onReachBottomDistance": 50 + } \ No newline at end of file diff --git a/pages/guanshipaihang/guanshipaihang.wxml b/pages/guanshipaihang/guanshipaihang.wxml new file mode 100644 index 0000000..ae0e82e --- /dev/null +++ b/pages/guanshipaihang/guanshipaihang.wxml @@ -0,0 +1,289 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 管事排行榜 + + GUANSHI RANKING + + + + + + + 今日充值人数 + + + + 今月充值人数 + + + + + + + + + + + + + + 数据加载中 + Connecting to server... + + + + + + + + + 📡 + + 排行榜数据准备中 + 当前显示为模拟数据,实时排名即将开放 + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + 👑 + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + 3 + + + + + + + + + + + + + + + 3 + + + + + + + + + + + + + 其他排名 + + + + + + + + {{xianshiZhenshiShuju ? index + 4 : index + 1}} + + + + + + + + + + {{item.nicheng}} + UID: {{item.uid}} + + + + {{item.jine}} + + + + + + + + + + + + 🏆 + 暂无排名数据 + 成为第一个登榜的管事吧 + + + + diff --git a/pages/guanshipaihang/guanshipaihang.wxss b/pages/guanshipaihang/guanshipaihang.wxss new file mode 100644 index 0000000..7dffd55 --- /dev/null +++ b/pages/guanshipaihang/guanshipaihang.wxss @@ -0,0 +1,1276 @@ +/* 管事排行榜和商家排行榜通用样式文件 */ +/* 保存为:pages/guanshi-paihang/guanshi-paihang.wxss 和 pages/shangjia-paihang/shangjia-paihang.wxss */ + +/* ========== 基础页面样式 ========== */ +.guanshi-paihang-page, +.shangjia-paihang-page { + min-height: 100vh; + background: #000; + color: #fff; + position: relative; + overflow-x: hidden; + padding: 40rpx 30rpx; + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; +} + +/* ========== 赛博朋克背景 ========== */ +.cyber-bg { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + pointer-events: none; +} + +.cyber-grid { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + linear-gradient(rgba(0, 255, 255, 0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 255, 255, 0.05) 1px, transparent 1px); + background-size: 50rpx 50rpx; + opacity: 0.3; +} + +.cyber-scanline { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3rpx; + background: linear-gradient(90deg, + transparent 0%, + #00ffff 30%, + #ff00ff 50%, + #00ffff 70%, + transparent 100%); + animation: scanline 3s linear infinite; + filter: blur(1rpx); + box-shadow: 0 0 20rpx #00ffff; + z-index: 2; +} + +@keyframes scanline { + 0% { top: 0; } + 100% { top: 100%; } +} + +.data-streams { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; +} + +.stream { + position: absolute; + width: 1rpx; + height: 100rpx; + background: linear-gradient(to bottom, transparent, #00ffff, transparent); + filter: blur(0.5rpx); + animation: streamFlow 8s linear infinite; +} + +.stream-1 { + left: 20%; + animation-delay: 0s; +} + +.stream-2 { + left: 50%; + animation-delay: 2s; +} + +.stream-3 { + left: 80%; + animation-delay: 4s; +} + +@keyframes streamFlow { + 0% { + top: -100rpx; + opacity: 0; + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + top: 100%; + opacity: 0; + } +} + +.floating-particles { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.particle { + position: absolute; + width: 6rpx; + height: 6rpx; + background: #00ffff; + border-radius: 50%; + filter: blur(1rpx); + box-shadow: 0 0 10rpx #00ffff; + animation: particleFloat 20s infinite linear; +} + +.particle:nth-child(odd) { + background: #ff00ff; + box-shadow: 0 0 10rpx #ff00ff; +} + +@keyframes particleFloat { + 0%, 100% { + transform: translate(0, 0); + } + 25% { + transform: translate(100rpx, -100rpx); + } + 50% { + transform: translate(-50rpx, 150rpx); + } + 75% { + transform: translate(150rpx, 50rpx); + } +} + +/* ========== 页面头部 ========== */ +.page-header { + position: relative; + z-index: 10; + margin-bottom: 55rpx; + margin-top: -40rpx; +} + +.title-container { + text-align: center; + margin-bottom: 40rpx; +} + +.main-title { + display: block; + font-size: 58rpx; + font-weight: 900; + background: linear-gradient(90deg, #00ffff, #ff00ff, #ffff00); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 0 0 30rpx rgba(0, 255, 255, 0.5); + letter-spacing: 4rpx; + margin-bottom: 8rpx; +} + +.title-line { + width: 220rpx; + height: 3rpx; + background: linear-gradient(90deg, transparent, #00ffff, #ff00ff, transparent); + margin: 12rpx auto; + position: relative; +} + +.title-line::before { + content: ''; + position: absolute; + top: -2rpx; + left: 0; + right: 0; + height: 7rpx; + background: linear-gradient(90deg, + transparent 0%, + rgba(0, 255, 255, 0.3) 20%, + rgba(255, 0, 255, 0.3) 80%, + transparent 100%); + filter: blur(4rpx); +} + +.sub-title { + display: block; + font-size: 22rpx; + color: rgba(255, 255, 255, 0.6); + letter-spacing: 6rpx; + text-transform: uppercase; + margin-top: 5rpx; +} + +.toggle-container { + display: flex; + background: rgba(10, 10, 30, 0.8); + border-radius: 60rpx; + padding: 6rpx; + position: relative; + border: 1rpx solid rgba(0, 255, 255, 0.2); + backdrop-filter: blur(20rpx); + max-width: 700rpx; + width: 90%; + margin: 0 auto; + box-shadow: + 0 10rpx 40rpx rgba(0, 0, 0, 0.8), + inset 0 0 20rpx rgba(0, 255, 255, 0.05); +} + +.toggle-bg { + position: absolute; + top: 6rpx; + bottom: 6rpx; + width: 50%; + background: linear-gradient(90deg, + rgba(0, 255, 255, 0.2), + rgba(255, 0, 255, 0.2)); + border-radius: 54rpx; + transition: transform 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); + z-index: 1; +} + +.toggle-btn { + flex: 1; + text-align: center; + padding: 30rpx 0; + font-size: 32rpx; + font-weight: 600; + color: rgba(255, 255, 255, 0.7); + position: relative; + z-index: 2; + transition: all 0.3s ease; + border-radius: 54rpx; + min-width: 200rpx; +} + +.toggle-btn.active { + color: #ffffff; + text-shadow: 0 0 15rpx rgba(255, 255, 255, 0.5); +} + +.toggle-btn.disabled { + opacity: 0.5; + pointer-events: none; +} + +.toggle-text { + position: relative; + z-index: 1; +} + +.toggle-line { + position: absolute; + bottom: 15rpx; + left: 50%; + transform: translateX(-50%); + width: 40rpx; + height: 3rpx; + background: linear-gradient(90deg, #00ffff, #ff00ff); + border-radius: 2rpx; + opacity: 0; + transition: opacity 0.3s ease; +} + +.toggle-line.active { + opacity: 1; + box-shadow: 0 0 15rpx rgba(0, 255, 255, 0.5); +} + +/* ========== 加载状态 ========== */ +.loading-state { + display: flex; + align-items: center; + justify-content: center; + height: 60vh; + position: relative; + z-index: 10; +} + +.loading-content { + text-align: center; +} + +.loading-spinner { + position: relative; + width: 120rpx; + height: 120rpx; + margin: 0 auto 30rpx; +} + +.spinner-outer { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 8rpx solid transparent; + border-top-color: #00ffff; + border-right-color: #ff00ff; + border-radius: 50%; + animation: spinOuter 2s linear infinite; +} + +.spinner-inner { + position: absolute; + top: 15rpx; + left: 15rpx; + right: 15rpx; + bottom: 15rpx; + border: 6rpx solid transparent; + border-bottom-color: #ffff00; + border-left-color: #00ffff; + border-radius: 50%; + animation: spinInner 1.5s linear infinite reverse; +} + +.spinner-core { + position: absolute; + top: 30rpx; + left: 30rpx; + right: 30rpx; + bottom: 30rpx; + background: radial-gradient(circle, + rgba(0, 255, 255, 0.2) 0%, + rgba(255, 0, 255, 0.1) 50%, + transparent 100%); + border-radius: 50%; +} + +@keyframes spinOuter { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes spinInner { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(-360deg); } +} + +.loading-text { + display: block; + font-size: 32rpx; + color: #00ffff; + margin-bottom: 10rpx; + text-shadow: 0 0 10rpx rgba(0, 255, 255, 0.5); +} + +.loading-sub { + display: block; + font-size: 24rpx; + color: rgba(255, 255, 255, 0.6); +} + +/* ========== 虚拟数据提示 ========== */ +.demo-notice { + background: rgba(15, 15, 35, 0.9); + border-radius: 25rpx; + padding: 30rpx; + margin-bottom: 40rpx; + position: relative; + z-index: 10; + border: 1rpx solid rgba(255, 0, 255, 0.3); + backdrop-filter: blur(20rpx); + display: flex; + align-items: center; + overflow: hidden; + box-shadow: + 0 10rpx 40rpx rgba(0, 0, 0, 0.8), + 0 0 30rpx rgba(255, 0, 255, 0.2); +} + +.notice-icon { + font-size: 60rpx; + margin-right: 30rpx; + background: linear-gradient(45deg, #ff00ff, #00ffff); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + flex-shrink: 0; +} + +.notice-content { + flex: 1; +} + +.notice-title { + display: block; + font-size: 32rpx; + font-weight: 700; + color: #ff00ff; + margin-bottom: 10rpx; + text-shadow: 0 0 15rpx rgba(255, 0, 255, 0.5); +} + +.notice-desc { + display: block; + font-size: 26rpx; + color: rgba(255, 255, 255, 0.7); + line-height: 1.4; +} + +.notice-glow { + position: absolute; + top: -100%; + left: -100%; + right: -100%; + bottom: -100%; + background: radial-gradient(circle at center, + rgba(255, 0, 255, 0.1) 0%, + transparent 70%); + z-index: -1; +} + +/* ========== 前三名区域 ========== */ +.top-three-section { + display: flex; + justify-content: center; + align-items: flex-end; + position: relative; + z-index: 10; + margin-bottom: 40rpx; + gap: 20rpx; + margin-top: 10rpx; +} + +.rank-card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + transition: all 0.3s ease; +} + +.rank-1 { + width: 280rpx; + z-index: 30; + margin-bottom: 30rpx; +} + +.rank-2 { + width: 220rpx; + z-index: 20; + margin-bottom: 0rpx; + margin-right: -20rpx; +} + +.rank-3 { + width: 220rpx; + z-index: 20; + margin-bottom: 0rpx; + margin-left: -20rpx; +} + +.rank-card.flash { + animation: cardFlash 0.4s ease-out; +} + +@keyframes cardFlash { + 0% { + filter: brightness(1) drop-shadow(0 0 0 rgba(255, 255, 255, 0)); + } + 50% { + filter: brightness(1.4) drop-shadow(0 0 40rpx rgba(255, 255, 255, 0.8)); + } + 100% { + filter: brightness(1) drop-shadow(0 0 0 rgba(255, 255, 255, 0)); + } +} + +/* 奖台底座 */ +.podium-base { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 60rpx; + border-radius: 30rpx 30rpx 0 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; +} + +.rank-1 .podium-base { + height: 80rpx; +} + +.podium-gold { + background: linear-gradient(180deg, + rgba(255, 215, 0, 0.3) 0%, + rgba(255, 215, 0, 0.1) 100%); + border: 1rpx solid rgba(255, 215, 0, 0.4); +} + +.podium-silver { + background: linear-gradient(180deg, + rgba(192, 192, 192, 0.3) 0%, + rgba(192, 192, 192, 0.1) 100%); + border: 1rpx solid rgba(192, 192, 192, 0.4); +} + +.podium-bronze { + background: linear-gradient(180deg, + rgba(205, 127, 50, 0.3) 0%, + rgba(205, 127, 50, 0.1) 100%); + border: 1rpx solid rgba(205, 127, 50, 0.4); +} + +.podium-glow { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 30rpx 30rpx 0 0; + background: radial-gradient(ellipse at center, + rgba(255, 255, 255, 0.2) 0%, + transparent 70%); + z-index: -1; +} + +.podium-number { + font-size: 32rpx; + font-weight: 900; + color: rgba(255, 255, 255, 0.9); + text-shadow: 0 0 10rpx currentColor; +} + +/* ========== 皇冠容器 ========== */ +.crown-container { + position: absolute; + top: -55rpx; + left: 55%; + transform: translateX(20%) rotate(35deg)!important; + width: 110rpx; + height: 110rpx; + z-index: 40; + display: flex; + align-items: center; + justify-content: center; +} + +.crown { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.crown-icon { + font-size: 85rpx; + position: relative; + z-index: 2; + filter: drop-shadow(0 0 10rpx rgba(255, 215, 0, 0.8)); + animation: crownFloat 3s ease-in-out infinite; + transform: rotate(5deg); +} + +@keyframes crownFloat { + 0%, 100% { + transform: translateY(0) rotate(5deg) scale(1); + filter: drop-shadow(0 0 10rpx rgba(255, 215, 0, 0.8)); + } + 50% { + transform: translateY(-15rpx) rotate(10deg) scale(1.1); + filter: drop-shadow(0 0 25rpx rgba(255, 215, 0, 1)); + } +} + +.crown-rays { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 180rpx; + height: 180rpx; + z-index: 1; +} + +.ray { + position: absolute; + top: 50%; + left: 50%; + width: 2rpx; + height: 80rpx; + background: linear-gradient(to top, + transparent, + rgba(255, 215, 0, 0.8), + rgba(255, 215, 0, 1), + rgba(255, 215, 0, 0.8), + transparent); + transform-origin: bottom center; + filter: blur(1rpx); +} + +.ray-1 { transform: translate(-50%, -100%) rotate(0deg); } +.ray-2 { transform: translate(-50%, -100%) rotate(60deg); } +.ray-3 { transform: translate(-50%, -100%) rotate(120deg); } +.ray-4 { transform: translate(-50%, -100%) rotate(180deg); } +.ray-5 { transform: translate(-50%, -100%) rotate(240deg); } +.ray-6 { transform: translate(-50%, -100%) rotate(300deg); } + +.crown-glow { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 200rpx; + height: 200rpx; + background: radial-gradient(circle, + rgba(255, 215, 0, 0.3) 0%, + rgba(255, 215, 0, 0.1) 40%, + transparent 70%); + z-index: 0; + animation: glowPulse 2s ease-in-out infinite; +} + +@keyframes glowPulse { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 0.8; } +} + +/* ========== 排名内容 ========== */ +.rank-content { + position: relative; + width: 100%; + z-index: 2; +} + +/* 头像框架 */ +.avatar-frame { + position: relative; + width: 160rpx; + height: 160rpx; + margin: 0 auto 30rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.rank-1 .avatar-frame { + width: 200rpx; + height: 200rpx; +} + +.frame-gold { + filter: drop-shadow(0 0 30rpx rgba(255, 215, 0, 0.5)); +} + +.frame-silver { + filter: drop-shadow(0 0 25rpx rgba(192, 192, 192, 0.5)); +} + +.frame-bronze { + filter: drop-shadow(0 0 25rpx rgba(205, 127, 50, 0.5)); +} + +.avatar-container { + position: relative; + width: 100%; + height: 100%; + border-radius: 50%; + overflow: hidden; + background: rgba(20, 20, 40, 0.8); + display: flex; + align-items: center; + justify-content: center; + border: 4rpx solid transparent; +} + +.rank-1 .avatar-container { + border-width: 6rpx; +} + +.frame-gold .avatar-container { + border-color: rgba(255, 215, 0, 0.3); +} + +.frame-silver .avatar-container { + border-color: rgba(192, 192, 192, 0.3); +} + +.frame-bronze .avatar-container { + border-color: rgba(205, 127, 50, 0.3); +} + +.avatar-image { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + position: relative; + z-index: 2; +} + +/* 头像光晕 */ +.avatar-halo { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 50%; + z-index: 1; + pointer-events: none; + animation: haloRotate 10s linear infinite; +} + +.halo-gold { + width: 220rpx; + height: 220rpx; + background: radial-gradient(circle, + rgba(255, 215, 0, 0.4) 0%, + rgba(255, 215, 0, 0.2) 30%, + transparent 70%); +} + +.rank-1 .halo-gold { + width: 260rpx; + height: 260rpx; +} + +.halo-silver { + width: 200rpx; + height: 200rpx; + background: radial-gradient(circle, + rgba(192, 192, 192, 0.4) 0%, + rgba(192, 192, 192, 0.2) 30%, + transparent 70%); +} + +.halo-bronze { + width: 200rpx; + height: 200rpx; + background: radial-gradient(circle, + rgba(205, 127, 50, 0.4) 0%, + rgba(205, 127, 50, 0.2) 30%, + transparent 70%); +} + +@keyframes haloRotate { + 0% { transform: translate(-50%, -50%) rotate(0deg); } + 100% { transform: translate(-50%, -50%) rotate(360deg); } +} + +/* 头像闪烁粒子 */ +.avatar-sparkles { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 50%; + z-index: 3; +} + +.sparkle { + position: absolute; + width: 8rpx; + height: 8rpx; + background: #fff; + border-radius: 50%; + filter: blur(1rpx); + opacity: 0; + animation: sparkleTwinkle 3s ease-in-out infinite; +} + +.rank-1 .sparkle { + width: 10rpx; + height: 10rpx; +} + +.s1 { top: 15%; left: 15%; animation-delay: 0s; } +.s2 { top: 25%; right: 25%; animation-delay: 0.5s; } +.s3 { bottom: 35%; left: 35%; animation-delay: 1s; } +.s4 { bottom: 15%; right: 15%; animation-delay: 1.5s; } + +@keyframes sparkleTwinkle { + 0%, 100% { + opacity: 0; + transform: scale(0.5); + } + 50% { + opacity: 1; + transform: scale(1); + } +} + +/* 排名徽章 */ +.rank-badge { + position: absolute; + bottom: -10rpx; + right: -10rpx; + width: 60rpx; + height: 60rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + font-weight: 900; + color: #fff; + z-index: 10; + border: 3rpx solid rgba(255, 255, 255, 0.3); + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.5); +} + +.rank-1 .rank-badge { + width: 70rpx; + height: 70rpx; + font-size: 32rpx; + bottom: -15rpx; + right: -15rpx; +} + +.badge-gold { + background: linear-gradient(135deg, #ffd700, #ff9800); +} + +.badge-silver { + background: linear-gradient(135deg, #c0c0c0, #808080); +} + +.badge-bronze { + background: linear-gradient(135deg, #cd7f32, #a0522d); +} + +/* 用户信息卡片 */ +.user-info-card { + background: rgba(15, 15, 35, 0.95); + border-radius: 25rpx; + padding: 30rpx 25rpx; + backdrop-filter: blur(20rpx); + border: 1rpx solid rgba(255, 255, 255, 0.1); + box-shadow: + 0 20rpx 60rpx rgba(0, 0, 0, 0.8), + inset 0 0 30rpx rgba(255, 255, 255, 0.05); + position: relative; + overflow: hidden; +} + +.rank-1 .user-info-card { + border-color: rgba(255, 215, 0, 0.3); + background: rgba(20, 15, 5, 0.95); +} + +.rank-2 .user-info-card { + border-color: rgba(192, 192, 192, 0.3); + background: rgba(25, 25, 35, 0.95); +} + +.rank-3 .user-info-card { + border-color: rgba(205, 127, 50, 0.3); + background: rgba(30, 20, 10, 0.95); +} + +.info-header { + margin-bottom: 20rpx; + text-align: center; +} + +.user-name { + font-size: 32rpx; + font-weight: 700; + color: #fff; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.rank-1 .user-name { + color: #ffd700; + text-shadow: 0 0 15rpx rgba(255, 215, 0, 0.7); + font-size: 34rpx; +} + +.rank-2 .user-name { + color: #c0c0c0; + text-shadow: 0 0 10rpx rgba(192, 192, 192, 0.7); +} + +.rank-3 .user-name { + color: #cd7f32; + text-shadow: 0 0 10rpx rgba(205, 127, 50, 0.7); +} + +.info-body { + border-top: 1rpx solid rgba(255, 255, 255, 0.1); + padding-top: 15rpx; +} + +.uid-row, .income-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12rpx; +} + +.uid-row:last-child, .income-row:last-child { + margin-bottom: 0; +} + +.uid-label, .income-label { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.6); +} + +.uid-value { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.9); + font-family: 'Courier New', monospace; +} + +.income-value { + display: flex; + align-items: baseline; +} + +.currency { + font-size: 26rpx; + color: #4ade80; + margin-right: 4rpx; +} + +.rank-1 .currency { + color: #ffd700; +} + +.rank-2 .currency { + color: #c0c0c0; +} + +.rank-3 .currency { + color: #cd7f32; +} + +.amount { + font-size: 32rpx; + font-weight: 700; + color: #4ade80; + text-shadow: 0 0 10rpx rgba(74, 222, 128, 0.5); +} + +.rank-1 .amount { + color: #ffd700; + text-shadow: 0 0 15rpx rgba(255, 215, 0, 0.7); +} + +.rank-2 .amount { + color: #c0c0c0; + text-shadow: 0 0 10rpx rgba(192, 192, 192, 0.7); +} + +.rank-3 .amount { + color: #cd7f32; + text-shadow: 0 0 10rpx rgba(205, 127, 50, 0.7); +} + +.unit { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.6); + margin-left: 4rpx; +} + +.amount.medium { + font-size: 30rpx; +} + +.amount.small { + font-size: 28rpx; +} + +.amount.very-small { + font-size: 26rpx; +} + +/* ========== 其他排名区域 ========== */ +.other-ranks-section { + position: relative; + z-index: 10; +} + +.section-title { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 40rpx; +} + +.title-line { + flex: 1; + height: 2rpx; + background: linear-gradient(90deg, + transparent, + #00ffff, + #ff00ff, + #00ffff, + transparent); +} + +.title-text { + font-size: 32rpx; + font-weight: 700; + color: #00ffff; + margin: 0 30rpx; + text-shadow: 0 0 15rpx rgba(0, 255, 255, 0.5); +} + +.rank-list { + position: relative; +} + +.rank-item { + display: flex; + align-items: center; + background: rgba(20, 20, 40, 0.8); + border-radius: 25rpx; + padding: 25rpx 30rpx; + margin-bottom: 20rpx; + border: 1rpx solid rgba(0, 255, 255, 0.1); + backdrop-filter: blur(20rpx); + position: relative; + overflow: hidden; + transition: all 0.3s ease; + box-shadow: + 0 10rpx 40rpx rgba(0, 0, 0, 0.6), + inset 0 0 20rpx rgba(0, 255, 255, 0.05); +} + +.rank-item.flash { + animation: cardFlash 0.4s ease-out; +} + +.rank-item::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + transparent, + rgba(0, 255, 255, 0.1), + transparent); + transition: left 0.6s ease; +} + +.rank-item:hover::before { + left: 100%; +} + +.rank-number { + width: 80rpx; + text-align: center; + position: relative; + margin-right: 30rpx; + flex-shrink: 0; +} + +.number-text { + font-size: 42rpx; + font-weight: 900; + color: rgba(255, 255, 255, 0.9); + position: relative; + z-index: 1; +} + +.number-glow { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 80rpx; + height: 80rpx; + background: radial-gradient(circle, + rgba(0, 255, 255, 0.2) 0%, + transparent 70%); + z-index: 0; +} + +.rank-avatar { + position: relative; + width: 100rpx; + height: 100rpx; + margin-right: 25rpx; + flex-shrink: 0; +} + +.rank-avatar-img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + position: relative; + z-index: 2; + border: 3rpx solid rgba(0, 255, 255, 0.2); +} + +.rank-avatar-halo { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 120rpx; + height: 120rpx; + background: radial-gradient(circle, + rgba(0, 255, 255, 0.2) 0%, + rgba(0, 255, 255, 0.1) 30%, + transparent 70%); + z-index: 1; + border-radius: 50%; +} + +.rank-info { + flex: 1; + min-width: 0; +} + +.rank-name { + display: block; + font-size: 32rpx; + font-weight: 600; + color: #ffffff; + margin-bottom: 8rpx; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.rank-uid { + display: block; + font-size: 24rpx; + color: rgba(255, 255, 255, 0.6); + font-family: 'Courier New', monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.rank-income { + position: relative; + flex-shrink: 0; + display: flex; + align-items: baseline; + margin-left: 20rpx; +} + +.income-currency { + font-size: 28rpx; + color: #00ffff; + margin-right: 4rpx; + opacity: 0.9; +} + +.income-amount { + font-size: 36rpx; + font-weight: 700; + color: #4ade80; + text-shadow: 0 0 10rpx rgba(74, 222, 128, 0.5); + white-space: nowrap; + line-height: 1; +} + +.income-unit { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.6); + margin-left: 4rpx; +} + +.income-amount.medium { + font-size: 34rpx; +} + +.income-amount.small { + font-size: 32rpx; +} + +.income-amount.very-small { + font-size: 30rpx; +} + +.income-glow { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 140rpx; + height: 70rpx; + background: radial-gradient(ellipse, + rgba(74, 222, 128, 0.2) 0%, + transparent 70%); + z-index: 0; +} + +.rank-hover { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 25rpx; + border: 1rpx solid transparent; + background: linear-gradient(45deg, + rgba(0, 255, 255, 0.1), + rgba(255, 0, 255, 0.1)) border-box; + mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0); + -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; +} + +.rank-item:hover .rank-hover { + opacity: 1; +} + +/* ========== 空状态 ========== */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 0; + text-align: center; + position: relative; + z-index: 10; +} + +.empty-icon { + font-size: 120rpx; + margin-bottom: 30rpx; + opacity: 0.5; + animation: iconFloat 3s ease-in-out infinite; +} + +@keyframes iconFloat { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-30rpx); } +} + +.empty-title { + font-size: 36rpx; + color: rgba(255, 255, 255, 0.8); + margin-bottom: 15rpx; +} + +.empty-desc { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.5); +} + +/* ========== 调整整个内容区域的上边距 ========== */ +.ranking-content { + position: relative; + z-index: 5; + margin-top: 10rpx; +} \ No newline at end of file diff --git a/pages/guanzhual/guanzhual.js b/pages/guanzhual/guanzhual.js new file mode 100644 index 0000000..ea8dbfd --- /dev/null +++ b/pages/guanzhual/guanzhual.js @@ -0,0 +1,90 @@ +// pages/guanzhual/guanzhual.js +const app = getApp() +import request from '../../utils/request.js' + +Page({ + data: { + images: [], // 图片完整URL列表 + lastUpdate: '', // 最新更新时间 + visitCount: 0, // 浏览次数 + loading: true, // 加载中 + ossImageUrl: '' // OSS基础地址 + }, + + onLoad() { + // 获取OSS地址 + this.setData({ + ossImageUrl: app.globalData.ossImageUrl || '' + }) + this.loadData() + }, + + onShow() { + this.registerNotificationComponent() + }, + + // 注册全局通知组件 + registerNotificationComponent() { + const notificationComp = this.selectComponent('#global-notification') + if (notificationComp && notificationComp.showNotification) { + app.globalData.globalNotification = { + show: (data) => notificationComp.showNotification(data), + hide: () => notificationComp.hideNotification() + } + } + }, + + // 加载数据 + loadData() { + this.setData({ loading: true }) + + request({ + url: '/peizhi/gzal', + method: 'POST', + header: { 'content-type': 'application/json' } + }) + .then(res => { + if (res && res.data && res.data.code === 0) { + const data = res.data.data || {} + const ossImageUrl = this.data.ossImageUrl + // 拼接完整URL + const fullImages = (data.images || []).map(url => { + if (url && !url.startsWith('http')) { + return ossImageUrl + url + } + return url + }) + this.setData({ + images: fullImages, + lastUpdate: data.last_update || '', + visitCount: data.visit_count || 0, + loading: false + }) + } else { + wx.showToast({ + title: res?.data?.msg || '加载失败', + icon: 'none' + }) + this.setData({ loading: false }) + } + }) + .catch(err => { + console.error('加载关注阿龙数据失败', err) + wx.showToast({ + title: '网络错误', + icon: 'none' + }) + this.setData({ loading: false }) + }) + }, + + // 图片加载成功(可选) + onImageLoad(e) { + // 可忽略 + }, + + // 图片加载失败 + onImageError(e) { + console.warn('图片加载失败', e) + } +}) \ No newline at end of file diff --git a/pages/guanzhual/guanzhual.json b/pages/guanzhual/guanzhual.json new file mode 100644 index 0000000..ac80b22 --- /dev/null +++ b/pages/guanzhual/guanzhual.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "关注二H", + "usingComponents": { + "global-notification": "/components/global-notification/global-notification" + }, + "enablePullDownRefresh": false, + "backgroundTextStyle": "dark" + } \ No newline at end of file diff --git a/pages/guanzhual/guanzhual.wxml b/pages/guanzhual/guanzhual.wxml new file mode 100644 index 0000000..5394ae8 --- /dev/null +++ b/pages/guanzhual/guanzhual.wxml @@ -0,0 +1,39 @@ + + + + + 关注快手 + {{visitCount}}次浏览 + + + 更新时间:{{lastUpdate}} + + + + + + + + + + + + 暂无内容 + + + + + + + 加载中... + + + + + \ No newline at end of file diff --git a/pages/guanzhual/guanzhual.wxss b/pages/guanzhual/guanzhual.wxss new file mode 100644 index 0000000..675b458 --- /dev/null +++ b/pages/guanzhual/guanzhual.wxss @@ -0,0 +1,95 @@ +/* pages/guanzhual/guanzhual.wxss */ +page { + background-color: #ffffff; + min-height: 100vh; + } + + .guanzhual-page { + padding-bottom: 30rpx; + box-sizing: border-box; + } + + /* 头部信息 */ + .header-info { + padding: 30rpx 20rpx 20rpx 20rpx; + border-bottom: 2rpx solid #f5f5f5; + } + + .title-section { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 12rpx; + } + + .main-title { + font-size: 40rpx; + font-weight: 600; + color: #333333; + } + + .visit-count { + font-size: 28rpx; + color: #999999; + } + + .sub-info { + display: flex; + justify-content: space-between; + font-size: 26rpx; + color: #aaaaaa; + } + + .update-time { + color: #888888; + } + + /* 图片列表 */ + .image-list { + padding: 0 8rpx; /* 非常微小的左右边距 */ + } + + .image-item { + width: 100%; + margin-bottom: 6rpx; /* 极小的上下间距 */ + background-color: #fafafa; + overflow: hidden; + border-radius: 4rpx; + } + + .image-item image { + width: 100%; + display: block; + } + + /* 空状态提示 */ + .empty-tip { + padding: 100rpx 0; + text-align: center; + color: #999999; + font-size: 28rpx; + } + + /* 加载状态 */ + .loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 0; + } + + .loading-spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid #e0e0e0; + border-top-color: #00C853; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 20rpx; + } + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } \ No newline at end of file diff --git a/pages/haibao/haibao.js b/pages/haibao/haibao.js new file mode 100644 index 0000000..3985d30 --- /dev/null +++ b/pages/haibao/haibao.js @@ -0,0 +1,281 @@ +// pages/haibao/haibao.js +import request from '../../utils/request.js'; + +const app = getApp(); + +Page({ + data: { + qrcodeUrl: '', + isLoading: false, + showPosterCanvas: false, + posterImageTemp: '', + avatarUrl: '', + uid: '', + nickName: '', + }, + + onLoad() { + this.loadQRCodeFromCache(); + this.loadUserInfo(); + }, + + onShow() { + this.registerNotificationComponent(); + }, + + registerNotificationComponent() { + const notificationComp = this.selectComponent('#global-notification'); + if (notificationComp && notificationComp.showNotification) { + app.globalData.globalNotification = { + show: (data) => notificationComp.showNotification(data), + hide: () => notificationComp.hideNotification() + }; + } + }, + + loadQRCodeFromCache() { + try { + const relativeUrl = wx.getStorageSync('haibao_url'); + if (relativeUrl) { + const fullUrl = this.getFullImageUrl(relativeUrl); + this.setData({ qrcodeUrl: fullUrl }); + } else { + this.setData({ qrcodeUrl: '' }); + } + } catch (error) { + console.error('读取缓存失败', error); + } + }, + + loadUserInfo() { + const touxiang = wx.getStorageSync('touxiang'); + const ossUrl = app.globalData.ossImageUrl || ''; + const defaultAvatar = ossUrl + (app.globalData.morentouxiang || 'avatar/default.jpg'); + let avatarUrl = defaultAvatar; + if (touxiang && typeof touxiang === 'string' && touxiang.trim() !== '') { + avatarUrl = this.getFullImageUrl(touxiang); + } + const uid = wx.getStorageSync('uid') || ''; + let nickName = app.globalData.nicheng || wx.getStorageSync('nicheng') || '管事'; + this.setData({ avatarUrl, uid, nickName }); + }, + + getFullImageUrl(url) { + if (!url) return ''; + const ossUrl = app.globalData.ossImageUrl || ''; + return url.startsWith('http') ? url : ossUrl + url; + }, + + refreshQRCode() { + if (this.data.isLoading) return; + this.setData({ isLoading: true }); + request({ + url: '/peizhi/guanshiewm', + method: 'POST', + }).then(res => { + if (res.data && res.data.code === 0) { + const relativeUrl = res.data.data.url; + if (relativeUrl) { + wx.setStorageSync('haibao_url', relativeUrl); + const fullUrl = this.getFullImageUrl(relativeUrl); + this.setData({ qrcodeUrl: fullUrl }); + wx.showToast({ title: '获取成功', icon: 'success' }); + } else { + wx.showToast({ title: '返回数据异常', icon: 'none' }); + } + } else { + wx.showToast({ title: res.data.msg || '获取失败', icon: 'none' }); + } + }).catch(err => { + console.error('获取海报失败', err); + wx.showToast({ title: '网络错误', icon: 'none' }); + }).finally(() => { + this.setData({ isLoading: false }); + }); + }, + + // 🔥【修改】保存二维码到相册(先下载网络图片) + saveToAlbum() { + if (!this.data.qrcodeUrl) { + wx.showToast({ title: '暂无二维码', icon: 'none' }); + return; + } + + wx.showLoading({ title: '保存中...', mask: true }); + wx.downloadFile({ + url: this.data.qrcodeUrl, + success: (res) => { + wx.hideLoading(); + if (res.statusCode === 200) { + // 调用已有的保存函数(传入临时路径) + this.downloadAndSave(res.tempFilePath); + } else { + wx.showToast({ title: '下载失败', icon: 'none' }); + } + }, + fail: (err) => { + wx.hideLoading(); + console.error('下载二维码失败', err); + wx.showToast({ title: '下载失败', icon: 'none' }); + } + }); + }, + + generateMyPoster() { + if (!this.data.qrcodeUrl) { + wx.showToast({ title: '请先生成二维码', icon: 'none' }); + return; + } + + wx.showLoading({ title: '生成海报中...', mask: true }); + + const ossUrl = app.globalData.ossImageUrl || ''; + const bgUrl = ossUrl + 'beijing/haibaobeijing.jpg'; + const urls = [bgUrl, this.data.qrcodeUrl, this.data.avatarUrl]; + + const promises = urls.map(url => this.loadImage(url)); + Promise.all(promises) + .then((images) => { + wx.hideLoading(); + this.setData({ showPosterCanvas: true }, () => { + setTimeout(() => { + this.drawPoster(images[0], images[1], images[2]); + }, 200); + }); + }) + .catch((err) => { + wx.hideLoading(); + wx.showToast({ title: '图片加载失败', icon: 'none' }); + console.error('图片加载失败', err); + }); + }, + + loadImage(url) { + return new Promise((resolve, reject) => { + wx.getImageInfo({ + src: url, + success: (res) => resolve(res.path), + fail: reject + }); + }); + }, + + drawPoster(bgPath, qrPath, avatarPath) { + const ctx = wx.createCanvasContext('posterCanvas', this); + const query = wx.createSelectorQuery().in(this); + query.select('.poster-canvas').boundingClientRect(rect => { + if (!rect) { + wx.showToast({ title: '画布获取失败', icon: 'none' }); + return; + } + const canvasWidth = rect.width; + const canvasHeight = rect.height; + + // 1. 绘制背景 + ctx.drawImage(bgPath, 0, 0, canvasWidth, canvasHeight); + + const qrSize = 100; + const margin = 5; + const qrX = canvasWidth - qrSize - margin; + const qrY = canvasHeight - qrSize - margin; + ctx.drawImage(qrPath, qrX, qrY, qrSize, qrSize); + + const avatarSize = 60; + const avatarX = margin + 20; + const avatarY = canvasHeight - avatarSize - margin; + ctx.save(); + ctx.beginPath(); + ctx.arc(avatarX + avatarSize/2, avatarY + avatarSize/2, avatarSize/2, 0, 2 * Math.PI); + ctx.clip(); + ctx.drawImage(avatarPath, avatarX, avatarY, avatarSize, avatarSize); + ctx.restore(); + + ctx.setFontSize(16); + ctx.setFillStyle('#333333'); + ctx.setTextAlign('left'); + + let nick = this.data.nickName || '管事'; + if (nick.length > 3) { + nick = nick.substring(0, 3) + '...'; + } + const nameText = nick; + + const textX = avatarX + avatarSize + 8; + const textY = avatarY + avatarSize/2 + 6; + ctx.fillText(nameText, textX, textY); + + ctx.setFontSize(18); + ctx.setFillStyle('#ffaa00'); + ctx.setTextAlign('left'); + const slogan = '快来加入我们 ✦'; + const sloganX = avatarX; + const sloganY = avatarY - 12; + ctx.fillText(slogan, sloganX, sloganY); + + ctx.draw(false, () => { + wx.canvasToTempFilePath({ + canvasId: 'posterCanvas', + success: (res) => { + this.setData({ posterImageTemp: res.tempFilePath }); + }, + fail: (err) => { + console.error('生成海报临时文件失败', err); + } + }, this); + }); + }).exec(); + }, + + savePoster() { + if (!this.data.posterImageTemp) { + wx.showToast({ title: '海报尚未生成', icon: 'none' }); + return; + } + this.downloadAndSave(this.data.posterImageTemp); + }, + + hidePoster() { + this.setData({ showPosterCanvas: false, posterImageTemp: '' }); + }, + + downloadAndSave(filePath) { + wx.getSetting({ + success: (res) => { + if (!res.authSetting['scope.writePhotosAlbum']) { + wx.authorize({ + scope: 'scope.writePhotosAlbum', + success: () => { + this._saveImage(filePath); + }, + fail: () => { + wx.showModal({ + title: '提示', + content: '需要您授权保存到相册', + success: (modalRes) => { + if (modalRes.confirm) { + wx.openSetting(); + } + } + }); + } + }); + } else { + this._saveImage(filePath); + } + } + }); + }, + + _saveImage(filePath) { + wx.saveImageToPhotosAlbum({ + filePath: filePath, + success: () => { + wx.showToast({ title: '保存成功', icon: 'success' }); + }, + fail: (err) => { + console.error('保存失败', err); + wx.showToast({ title: '保存失败', icon: 'none' }); + } + }); + }, +}); \ No newline at end of file diff --git a/pages/haibao/haibao.json b/pages/haibao/haibao.json new file mode 100644 index 0000000..8f6fba7 --- /dev/null +++ b/pages/haibao/haibao.json @@ -0,0 +1,8 @@ +{ + "usingComponents": { + "global-notification": "/components/global-notification/global-notification" + }, + "navigationBarTitleText": "推广海报", + "navigationBarBackgroundColor": "#0a0e17", + "navigationBarTextStyle": "white" + } \ No newline at end of file diff --git a/pages/haibao/haibao.wxml b/pages/haibao/haibao.wxml new file mode 100644 index 0000000..aa2c48d --- /dev/null +++ b/pages/haibao/haibao.wxml @@ -0,0 +1,71 @@ + + + + + + + + + + ⚡ 推广海报 ⚡ + 邀请打手扫码注册,自动填充邀请码 + + + + + + + + + + + 点击下方按钮生成您的专属二维码 + + + + + + + + 📥 + 保存二维码 + + + + {{qrcodeUrl ? '重新获取' : '获取二维码'}} + + + + 生成中... + + + + 🎨 + 生成我的专属海报 + + + + + + + + + + 📥 + 保存海报 + + + ↩️ + 返回 + + + + + + + * 打手扫码后自动进入注册页面,邀请码将自动填充 + + + + + \ No newline at end of file diff --git a/pages/haibao/haibao.wxss b/pages/haibao/haibao.wxss new file mode 100644 index 0000000..e52db1f --- /dev/null +++ b/pages/haibao/haibao.wxss @@ -0,0 +1,269 @@ +/* 机甲风格推广海报页面(含新增专属海报样式) */ + +page { + background: #0a0e17; + min-height: 100vh; + font-family: 'Avenir', 'PingFang SC', 'Helvetica Neue', sans-serif; + color: #fff; + } + + /* 背景网格 */ + .grid-bg { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + linear-gradient(rgba(0, 160, 255, 0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 160, 255, 0.05) 1px, transparent 1px); + background-size: 50rpx 50rpx; + pointer-events: none; + z-index: 0; + } + + .page-container { + position: relative; + z-index: 2; + min-height: 100vh; + padding: 30rpx 25rpx; + box-sizing: border-box; + } + + /* 机械装饰线条 */ + .mech-line { + position: absolute; + pointer-events: none; + z-index: 1; + } + .line-h { + top: 200rpx; + left: 0; + width: 100%; + height: 2rpx; + background: linear-gradient(90deg, transparent, #00a6ff, transparent); + } + .line-v { + right: 50rpx; + top: 100rpx; + width: 2rpx; + height: 300rpx; + background: linear-gradient(180deg, transparent, #00a6ff, transparent); + } + + .content { + position: relative; + z-index: 3; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 40rpx 0; + } + + /* 标题区域 */ + .title-section { + text-align: center; + margin-bottom: 60rpx; + } + .title { + font-size: 44rpx; + font-weight: bold; + color: #fff; + text-shadow: 0 0 30rpx #00a6ff; + letter-spacing: 2rpx; + display: block; + margin-bottom: 20rpx; + } + .subtitle { + font-size: 28rpx; + color: #9cf; + opacity: 0.9; + letter-spacing: 1rpx; + } + + /* 二维码区域(方形,无斜切) */ + .qrcode-section { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 60rpx; + } + .qrcode-wrapper { + position: relative; + width: 400rpx; + height: 400rpx; + background: rgba(0, 20, 40, 0.8); + border: 4rpx solid #00a6ff; + /* 移除 clip-path,保持方形 */ + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0 80rpx rgba(0, 166, 255, 0.4); + animation: qrcodePulse 2s infinite alternate; + } + .qrcode-img { + width: 90%; + height: 90%; + object-fit: contain; + } + .qrcode-glow { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at 30% 30%, rgba(0, 166, 255, 0.2), transparent 70%); + pointer-events: none; + } + .empty-qrcode { + width: 400rpx; + height: 400rpx; + background: rgba(0, 20, 40, 0.8); + border: 4rpx dashed #00a6ff; + /* 方形 */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + .empty-icon { + font-size: 80rpx; + color: #00a6ff; + margin-bottom: 20rpx; + opacity: 0.5; + } + .empty-text { + font-size: 26rpx; + color: #9cf; + text-align: center; + padding: 0 40rpx; + } + + /* 按钮区域(机甲斜切风格) */ + .button-section { + width: 100%; + display: flex; + justify-content: center; + margin-bottom: 40rpx; + } + .btn-group { + display: flex; + gap: 20rpx; + flex-wrap: wrap; + justify-content: center; + } + .btn { + width: 220rpx; + height: 80rpx; + background: rgba(0, 30, 60, 0.8); + border: 2rpx solid #00a6ff; + border-right: none; + border-bottom: none; + clip-path: polygon(0 0, 100% 0, 90% 100%, 0 100%); + display: flex; + align-items: center; + justify-content: center; + position: relative; + transition: 0.2s; + cursor: pointer; + } + .btn:active { + transform: scale(0.96); + box-shadow: 0 0 40rpx #00a6ff; + } + .save-btn { + border-color: #00a6ff; + } + .refresh-btn { + border-color: #ffaa00; + } + .refresh-btn .btn-icon { + color: #ffaa00; + } + /* 🔥【新增】生成海报按钮样式 */ + .poster-btn { + border-color: #ffaa00; + } + .poster-btn .btn-icon { + color: #ffaa00; + } + .loading-btn { + opacity: 0.7; + pointer-events: none; + } + .btn-icon { + font-size: 36rpx; + margin-right: 8rpx; + } + .btn-text { + font-size: 26rpx; + font-weight: 500; + color: #fff; + } + .loading-spinner-small { + width: 30rpx; + height: 30rpx; + border: 4rpx solid rgba(255,255,255,0.2); + border-top: 4rpx solid #fff; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-right: 10rpx; + } + + /* 🔥【新增】专属海报展示区 */ + .poster-section { + margin-top: 40rpx; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + } + .poster-canvas { + width: 600rpx; + height: 900rpx; + background: #1a1f2e; /* 占位色 */ + border: 4rpx solid #00a6ff; + box-shadow: 0 0 60rpx #00a6ff; + /* 保持方形,无斜切 */ + } + .poster-actions { + display: flex; + gap: 30rpx; + margin-top: 30rpx; + justify-content: center; + } + .save-poster-btn { + background: linear-gradient(45deg, #0066cc, #00a6ff); + border-color: #00a6ff; + } + .back-btn { + background: rgba(0, 30, 60, 0.8); + border-color: #00a6ff; + } + + /* 使用说明(保留) */ + .instruction { + margin-top: 30rpx; + padding: 20rpx 40rpx; + background: rgba(0, 30, 50, 0.5); + border-left: 4rpx solid #00a6ff; + clip-path: polygon(0 0, 100% 0, 98% 100%, 0 100%); + } + .instruction-text { + font-size: 24rpx; + color: #9cf; + line-height: 1.6; + } + + /* 动画 */ + @keyframes qrcodePulse { + 0% { box-shadow: 0 0 40rpx rgba(0, 166, 255, 0.3); } + 100% { box-shadow: 0 0 100rpx rgba(0, 166, 255, 0.6); } + } + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } \ No newline at end of file diff --git a/pages/index/index.js b/pages/index/index.js new file mode 100644 index 0000000..d89bade --- /dev/null +++ b/pages/index/index.js @@ -0,0 +1,494 @@ +// pages/shangpin/shangpin.js +const app = getApp() + +Page({ + /** + * 页面的初始数据 + */ + data: { + // 全局变量引用 + ossImageUrl: '', + lunbozhanwei: '', + + // 页面数据 + shangpingonggao: '', + lunboList: [], + shangpinleixing: [], + shangpinzhuanqu: [], + shangpinliebiao: [], + + // 状态控制 + selectedLeixingId: null, + filteredData: [], + showGonggaoModal: false, + gonggaoAnim: true, + isLoading: false, + isRefreshing: false, + hasInitialized: false, + + // 🔴【新增】当前时间(用于弹窗显示) + currentTime: '' + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + // 🔥 等待全局配置加载完成 + this.waitForConfigAndInit(); + // 🆕 修改:仅在已登录时检查弹窗,避免未登录时发起请求导致 401 弹窗 + const token = wx.getStorageSync('token'); + if (token) { + PopupService.checkAndShow(this, 'index'); + } + }, + // 🆕 页面隐藏时清理弹窗视图 + onHide: function () { + const popupComp = this.selectComponent('#popupNotice'); + if (popupComp && popupComp.cleanup) { + popupComp.cleanup(); + } +}, + + + /** + * 🔥 新增:等待配置加载完成再初始化 + */ + waitForConfigAndInit() { + if (app.globalData.ossImageUrl) { + // 配置已就绪,直接初始化 + this.setData({ + ossImageUrl: app.globalData.ossImageUrl, + lunbozhanwei: app.globalData.lunbozhanwei, + currentTime: this.getCurrentTime() + }); + this.initPageData(); + } else { + // 配置未就绪,延迟重试(最多等待3秒,每100ms检查一次) + + setTimeout(() => this.waitForConfigAndInit(), 100); + } + }, + + /** + * 生命周期函数--监听页面显示 + */ + onShow() { + // 如果已经初始化过,只更新时间;否则等待配置 + if (this.data.ossImageUrl) { + this.setData({ + gonggaoAnim: true, + currentTime: this.getCurrentTime() + }); + } + this.registerNotificationComponent(); + }, + + // 注册通知组件 + registerNotificationComponent() { + const app = getApp(); + const notificationComp = this.selectComponent('#global-notification'); + + if (notificationComp && notificationComp.showNotification) { + app.globalData.globalNotification = { + show: (data) => notificationComp.showNotification(data), + hide: () => notificationComp.hideNotification() + }; + } + }, + + /** + * 🔴【新增】获取当前时间(格式化) + */ + getCurrentTime() { + const now = new Date(); + const year = now.getFullYear(); + const month = (now.getMonth() + 1).toString().padStart(2, '0'); + const day = now.getDate().toString().padStart(2, '0'); + const hour = now.getHours().toString().padStart(2, '0'); + const minute = now.getMinutes().toString().padStart(2, '0'); + return `${year}-${month}-${day} ${hour}:${minute}`; + }, + + /** + * 🔴【新增】按paixu字段排序函数 + * 规则:1. paixu大的在前 2. paixu相同时id小的在前 3. 没有paixu的按id排序 + */ + sortByPaixu(list) { + if (!list || !Array.isArray(list)) return [] + + return [...list].sort((a, b) => { + // 获取paixu值,如果没有则默认为0 + const paixuA = a.paixu || 0 + const paixuB = b.paixu || 0 + + // 1. 先按paixu降序(大的在前) + if (paixuB !== paixuA) { + return paixuB - paixuA + } + + // 2. paixu相同时,按id升序(小的在前) + return a.id - b.id + }) + }, + + /** + * 初始化页面数据 + */ + initPageData() { + const that = this + this.setData({ isLoading: true }) + + const needLoad = [] + + // 1. 检查公告和轮播图 + if (!app.globalData.shangpingonggao || app.globalData.shangpinlunbo.length === 0) { + needLoad.push(this.loadGonggaoAndLunbo()) + } else { + this.setData({ + shangpingonggao: app.globalData.shangpingonggao, + lunboList: this.processImageUrls(app.globalData.shangpinlunbo) + }) + } + + // 2. 检查商品数据 + if (app.globalData.shangpinleixing.length === 0) { + needLoad.push(this.loadAllShangpinData()) + } else { + const leixingList = app.globalData.shangpinleixing + + // 🔴【修改】对商品类型进行排序 + const sortedLeixing = this.sortByPaixu(leixingList) + + this.setData({ + shangpinleixing: leixingList, + shangpinzhuanqu: app.globalData.shangpinzhuanqu || [], + shangpinliebiao: app.globalData.shangpinliebiao || [], + selectedLeixingId: sortedLeixing[0]?.id || null + }, () => { + this.filterShangpinData() + }) + } + + if (needLoad.length > 0) { + Promise.all(needLoad) + .then(() => { + that.setData({ isLoading: false }) + }) + .catch(error => { + console.error('数据加载失败:', error) + that.setData({ isLoading: false }) + wx.showToast({ + title: '加载失败,请重试', + icon: 'none' + }) + }) + } else { + this.setData({ isLoading: false }) + } + }, + + /** + * 加载公告和轮播图数据 + */ + loadGonggaoAndLunbo() { + const that = this + return new Promise((resolve, reject) => { + wx.request({ + url: app.globalData.apiBaseUrl + '/peizhi/shangpingonggao/', + method: 'POST', + header: { + 'content-type': 'application/json' + }, + success(res) { + if (res.statusCode === 200 && res.data) { + const data = res.data + + app.globalData.shangpingonggao = data.shangpingonggao || '' + app.globalData.shangpinlunbo = data.shangpinlunbo || [] + + that.setData({ + shangpingonggao: app.globalData.shangpingonggao, + lunboList: that.processImageUrls(app.globalData.shangpinlunbo) + }) + + resolve() + } else { + reject(new Error('请求失败')) + } + }, + fail(error) { + console.error('加载公告失败:', error) + reject(error) + } + }) + }) + }, + + /** + * 加载所有商品数据 + */ + loadAllShangpinData() { + const that = this + return new Promise((resolve, reject) => { + wx.request({ + url: app.globalData.apiBaseUrl + '/shangpin/shangpinhuoqu/', + method: 'POST', + header: { + 'content-type': 'application/json' + }, + success(res) { + if (res.statusCode === 200 && res.data) { + const data = res.data + + // 🔴【修改】对返回的数据进行排序 + const sortedLeixing = that.sortByPaixu(data.shangpinleixing || []) + const sortedZhuanqu = that.sortByPaixu(data.shangpinzhuanqu || []) + const sortedShangpin = that.sortByPaixu(data.shangpinliebiao || []) + + app.globalData.shangpinleixing = sortedLeixing + app.globalData.shangpinzhuanqu = sortedZhuanqu + app.globalData.shangpinliebiao = sortedShangpin + + that.setData({ + shangpinleixing: sortedLeixing, + shangpinzhuanqu: sortedZhuanqu, + shangpinliebiao: sortedShangpin, + selectedLeixingId: sortedLeixing[0]?.id || null + }, () => { + that.filterShangpinData() + resolve() + }) + } else { + reject(new Error('请求失败')) + } + }, + fail(error) { + console.error('加载商品数据失败:', error) + reject(error) + } + }) + }) + }, + + /** + * 处理图片URL(拼接OSS域名) + */ + processImageUrls(urlList) { + if (!urlList || !Array.isArray(urlList)) return [] + + return urlList.map(url => { + if (url.startsWith('http')) { + return url + } else { + return this.data.ossImageUrl + url + } + }) + }, + + /** + * 筛选商品数据 + */ + filterShangpinData() { + const { selectedLeixingId, shangpinzhuanqu, shangpinliebiao } = this.data + + if (!selectedLeixingId || !shangpinzhuanqu || !shangpinliebiao) { + this.setData({ filteredData: [] }) + return + } + + // 1. 构建专区Map(按类型分组,但只处理当前选中的类型) + const zhuanquByLeixing = new Map() + + shangpinzhuanqu.forEach(zhuanqu => { + if (zhuanqu.leixing_id === selectedLeixingId) { + if (!zhuanquByLeixing.has(selectedLeixingId)) { + zhuanquByLeixing.set(selectedLeixingId, []) + } + zhuanquByLeixing.get(selectedLeixingId).push(zhuanqu) + } + }) + + // 2. 构建商品Map(按专区分组) + const shangpinByZhuanqu = new Map() + + shangpinliebiao.forEach(shangpin => { + if (shangpin.leixing_id === selectedLeixingId && shangpin.zhuanqu_id) { + const zhuanquId = shangpin.zhuanqu_id + + if (!shangpinByZhuanqu.has(zhuanquId)) { + shangpinByZhuanqu.set(zhuanquId, []) + } + + // 处理价格显示 + const jiage = parseFloat(shangpin.jiage) || 0 + const priceInteger = Math.floor(jiage) + let priceDecimal = '00' + + const decimalPart = (jiage - priceInteger).toFixed(2) + if (decimalPart > 0) { + priceDecimal = decimalPart.toString().split('.')[1] + } + + shangpinByZhuanqu.get(zhuanquId).push({ + id: shangpin.id, + biaoqian: shangpin.biaoqian, + jiage: jiage, + priceInteger: priceInteger, + priceDecimal: priceDecimal, + tupian_url: shangpin.tupian_url, + duiwai_xiaoliang: shangpin.duiwai_xiaoliang || 0, + paixu: shangpin.paixu || 0 // 🔴 保留排序字段 + }) + } + }) + + // 3. 构建最终数据结构 + const filteredData = [] + const currentZhuanquList = zhuanquByLeixing.get(selectedLeixingId) || [] + + currentZhuanquList.forEach(zhuanqu => { + const shangpinList = shangpinByZhuanqu.get(zhuanqu.id) || [] + + if (shangpinList.length > 0) { + filteredData.push({ + zhuanqu: { + id: zhuanqu.id, + mingzi: zhuanqu.mingzi, + paixu: zhuanqu.paixu || 0 // 🔴 保留排序字段 + }, + shangpinList: shangpinList + }) + } + }) + + // 🔴【修改】对最终数据进行排序(确保专区顺序) + const sortedFilteredData = filteredData.sort((a, b) => { + // 按专区paixu降序 + const paixuA = a.zhuanqu.paixu || 0 + const paixuB = b.zhuanqu.paixu || 0 + if (paixuB !== paixuA) return paixuB - paixuA + // paixu相同时按专区id升序 + return a.zhuanqu.id - b.zhuanqu.id + }) + + this.setData({ filteredData: sortedFilteredData }) + }, + + /** + * 选择商品类型 + */ + selectLeixing(e) { + const leixingId = e.currentTarget.dataset.id + + if (this.data.selectedLeixingId === leixingId) { + return + } + + this.setData({ + selectedLeixingId: leixingId + }, () => { + this.filterShangpinData() + }) + }, + + /** + * 显示公告详情 + */ + showGonggaoDetail() { + this.setData({ + showGonggaoModal: true, + gonggaoAnim: false, + currentTime: this.getCurrentTime() // 🔴 更新弹窗时间 + }) + }, + + /** + * 隐藏公告详情 + */ + hideGonggaoDetail() { + this.setData({ + showGonggaoModal: false, + gonggaoAnim: true + }) + }, + + /** + * 刷新所有数据 + */ + refreshAllData() { + const that = this + this.setData({ isRefreshing: true }) + + wx.showLoading({ + title: '刷新中...', + mask: true + }) + + // 清空全局变量 + app.globalData.shangpingonggao = '' + app.globalData.shangpinlunbo = [] + app.globalData.shangpinleixing = [] + app.globalData.shangpinzhuanqu = [] + app.globalData.shangpinliebiao = [] + + Promise.all([ + that.loadGonggaoAndLunbo(), + that.loadAllShangpinData() + ]) + .then(() => { + wx.hideLoading() + that.setData({ isRefreshing: false }) + wx.showToast({ + title: '刷新成功', + icon: 'success', + duration: 1500 + }) + }) + .catch(error => { + console.error('刷新失败:', error) + wx.hideLoading() + that.setData({ isRefreshing: false }) + wx.showToast({ + title: '刷新失败', + icon: 'none' + }) + }) + }, + + /** + * 跳转到商品详情页 + */ + goToDetail(e) { + const shangpinId = e.currentTarget.dataset.id + if (!shangpinId) return + + wx.navigateTo({ + url: `/pages/shangpinxiangqing/shangpinxiangqing?id=${shangpinId}` + }) + }, + + /** + * 页面上拉触底事件的处理函数 + */ + onReachBottom() { + // 可以在这里实现分页加载 + }, + + /** + * 用户点击右上角分享 + */ + onShareAppMessage() { + return { + title: '阿龙电竞', + path: '/pages/shangpin/shangpin' + } + }, + + /** + * 页面卸载时清理 + */ + onUnload() { + this.setData({ gonggaoAnim: false }) + } +}) \ No newline at end of file diff --git a/pages/index/index.json b/pages/index/index.json new file mode 100644 index 0000000..06f55f5 --- /dev/null +++ b/pages/index/index.json @@ -0,0 +1,12 @@ +{ + "usingComponents": { + "global-notification": "/components/global-notification/global-notification", + "custom-tab-bar": "/custom-tab-bar/index" + }, + + "navigationBarTitleText": "商城", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTextStyle": "black", + + "backgroundColor": "#f5faff" + } \ No newline at end of file diff --git a/pages/index/index.wxml b/pages/index/index.wxml new file mode 100644 index 0000000..d5fff6b --- /dev/null +++ b/pages/index/index.wxml @@ -0,0 +1,172 @@ + + + + + + + + + + {{shangpingonggao || '暂无公告'}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{item.jieshao}} + + + + + + + + + + + + + + {{item.zhuanqu.mingzi}} + + + + + + + + + + + + + + + {{item.biaoqian}} + + + + + + ¥ + {{item.priceInteger}} + .{{item.priceDecimal}} + + + + + + + + + + + + + + 暂无商品 + + + + + + + + + + + 公告详情 + × + + + {{shangpingonggao || '暂无公告内容'}} + + + + + + + + 加载中... + + + + + + \ No newline at end of file diff --git a/pages/index/index.wxss b/pages/index/index.wxss new file mode 100644 index 0000000..beb4936 --- /dev/null +++ b/pages/index/index.wxss @@ -0,0 +1,585 @@ +/* ===== 1. 全局样式 ===== */ +.shangpin-page { + min-height: 100vh; + background: linear-gradient(180deg, #f5faff 0%, #f0f8ff 100%); + } + +/* ===== 2. 公告栏样式 - 物理上移方案 ===== */ +.gonggao-box { + margin: 20rpx 30rpx; + height: 80rpx; + border-radius: 40rpx; + background: linear-gradient(135deg, + rgba(220, 255, 220, 0.85) 0%, + rgba(255, 255, 200, 0.85) 100%); + backdrop-filter: blur(10px); + display: flex; + align-items: center; + padding: 0 25rpx; + box-shadow: 0 4rpx 15rpx rgba(0, 100, 0, 0.08); + border: 1rpx solid rgba(255, 255, 255, 0.3); + position: relative; + overflow: hidden; + } + + .gonggao-icon { + width: 40rpx; + height: 40rpx; + margin-right: 20rpx; + flex-shrink: 0; + display: flex; + align-items: center; + } + + .gonggao-icon image { + width: 100%; + height: 100%; + } + + .gonggao-content { + flex: 1; + overflow: hidden; + height: 100%; + position: relative; + } + + /* 关键修改:使用物理上移 */ + .gonggao-text { + font-size: 26rpx; + color: #333; + white-space: nowrap; + padding-right: 50rpx; + + /* 物理上移方法1:直接调整位置 */ + position: absolute; + top: -5rpx; /* 从顶部开始计算位置 */ + left: 0; + + /* 或者使用方法2:负margin上移 */ + /* position: relative; + top: -10rpx; */ + + /* 或者使用方法3:translate上移 */ + /* transform: translateY(-5rpx); */ + } + + /* 尝试不同的上移值,直到居中为止 */ + /* 可以尝试的值:5rpx, 8rpx, 10rpx, 12rpx, 15rpx, 18rpx, 20rpx */ + + .gonggao-anim { + animation: marquee 10s linear infinite; /* 加快速度到10秒 */ + animation-delay: 0.5s; + } + + @keyframes marquee { + 0% { + transform: translateX(100%); + } + 100% { + transform: translateX(-100%); + } + } + + /* ===== 3. 轮播图样式 ===== */ + .lunbo-container { + margin: 0 30rpx 30rpx; + height: 350rpx; + border-radius: 25rpx; + overflow: hidden; + position: relative; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1); + } + + .lunbo-swiper { + width: 100%; + height: 100%; + } + + .lunbo-image { + width: 100%; + height: 100%; + } + + .lunbo-placeholder { + width: 100%; + height: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + justify-content: center; + align-items: center; + } + + .lunbo-placeholder image { + width: 100%; + height: 100%; + opacity: 0.8; + } + + /* 刷新按钮 */ + .refresh-btn { + position: absolute; + top: 20rpx; + right: 20rpx; + width: 80rpx; + height: 80rpx; + z-index: 10; + display: flex; + justify-content: center; + align-items: center; + } + + .refresh-icon { + width: 60rpx; + height: 60rpx; + transition: transform 0.3s; + filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.2)); + } + + .refreshing { + animation: rotate360 1s linear infinite; + } + + @keyframes rotate360 { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + + .refresh-dot { + position: absolute; + width: 12rpx; + height: 12rpx; + background: #ffcc00; + border-radius: 50%; + top: 15rpx; + right: 15rpx; + animation: pulse 2s infinite; + } + + @keyframes pulse { + 0%, 100% { + opacity: 0.8; + transform: scale(1); + } + 50% { + opacity: 1; + transform: scale(1.2); + } + } + + /* ===== 4. 商品展示区域 ===== */ + .shangpin-container { + background: rgba(255, 255, 255, 0.95); + border-radius: 40rpx 40rpx 0 0; + margin-top: -20rpx; + padding: 40rpx 30rpx 100rpx; + min-height: 800rpx; + box-shadow: 0 -10rpx 30rpx rgba(0, 0, 0, 0.05); + } + + /* 商品类型选择 - 完全重新设计,确保左对齐且间距协调 */ + .leixing-scroll { + width: 100%; + white-space: nowrap; + margin-bottom: 50rpx; + } + + .leixing-list { + display: inline-flex; + padding-left: 0; /* 完全去掉左内边距,100%左对齐 */ + padding-right: 30rpx; /* 只保留右内边距给最后一个元素一点空间 */ + padding-top: 10rpx; + padding-bottom: 10rpx; + } + + /* 关键:完全协调的间距 */ + .leixing-item { + display: inline-flex; + flex-direction: column; + align-items: center; + margin-right: 30rpx; /* 合适的间距,不会太大也不会太小 */ + transition: all 0.3s; + position: relative; + flex-shrink: 0; + width: 130rpx; /* 固定宽度,确保四个看起来协调 */ + } + + .leixing-item:last-child { + margin-right: 0; + } + + .leixing-image-box { + width: 110rpx; /* 稍微减小,让整体更紧凑 */ + height: 110rpx; + border-radius: 25rpx; + overflow: hidden; + position: relative; + margin-bottom: 12rpx; + transition: all 0.3s; + box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.1); + } + + .leixing-image { + width: 100%; + height: 100%; + } + + .leixing-glow { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 25rpx; + border: 4rpx solid transparent; + transition: all 0.3s; + } + + .leixing-active .leixing-glow { + border-color: #ffcc00; + box-shadow: 0 0 20rpx rgba(255, 204, 0, 0.5); + } + + .leixing-active .leixing-image-box { + transform: translateY(-8rpx); + } + + .leixing-text { + font-size: 24rpx; + color: #666; + white-space: nowrap; + transition: all 0.3s; + text-align: center; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + } + + .leixing-active .leixing-text { + color: #333; + font-weight: bold; + transform: scale(1.05); + } + + /* 专区标题 */ + .zhuanqu-title { + display: flex; + align-items: center; + margin: 40rpx 0 15rpx; + padding-left: 0; + position: relative; + } + + .zhuanqu-icon { + width: 40rpx; + height: 40rpx; + margin-right: 20rpx; + position: relative; + } + + .zhuanqu-icon-image { + width: 100%; + height: 100%; + filter: drop-shadow(0 2rpx 4rpx rgba(255, 204, 0, 0.3)); + } + + .zhuanqu-sparkle { + animation: sparkle 2s infinite; + } + + @keyframes sparkle { + 0%, 100% { + transform: scale(1) rotate(0deg); + filter: drop-shadow(0 2rpx 4rpx rgba(255, 204, 0, 0.3)); + } + 50% { + transform: scale(1.1) rotate(10deg); + filter: drop-shadow(0 4rpx 8rpx rgba(255, 204, 0, 0.6)); + } + } + + .zhuanqu-text { + font-size: 32rpx; + font-weight: bold; + color: #333; + position: relative; + } + + .zhuanqu-text::after { + content: ''; + position: absolute; + left: 0; + bottom: -8rpx; + width: 60rpx; + height: 4rpx; + background: linear-gradient(90deg, #ffcc00, transparent); + border-radius: 2rpx; + } + + /* 商品网格 */ + .shangpin-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 25rpx; + margin-bottom: 40rpx; + } + + .shangpin-card { + background: #ffffff; + border-radius: 20rpx; + overflow: hidden; + box-shadow: 0 8rpx 25rpx rgba(0, 0, 0, 0.06); + transition: all 0.3s; + position: relative; + } + + .shangpin-card:active { + transform: scale(0.98); + box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.1); + } + + .shangpin-image-box { + width: 100%; + height: 220rpx; + position: relative; + overflow: hidden; + } + + .shangpin-image { + width: 100%; + height: 100%; + transition: transform 0.5s; + } + + /* 商品标题区域 - 协调紧凑 */ + .shangpin-title { + height: 75rpx; + padding: 6rpx 12rpx 2rpx 12rpx; + display: flex; + align-items: flex-start; + overflow: hidden; + } + + .title-text { + font-size: 26rpx; + color: #000; + line-height: 34rpx; + font-weight: 450; /* 微调粗细,更加协调 */ + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + } + + /* 价格和购买按钮 - 协调紧凑 */ + .shangpin-footer { + height: 50rpx; + padding: 0 12rpx 8rpx 12rpx; + display: flex; + justify-content: space-between; + align-items: center; + } + + .price-box { + display: flex; + align-items: baseline; + } + + .price-icon { + font-size: 24rpx; + color: #ff4444; + font-weight: bold; + margin-right: 2rpx; + } + + .price-integer { + font-size: 32rpx; + color: #ff4444; + font-weight: bold; + } + + .price-decimal { + font-size: 24rpx; + color: #ff4444; + font-weight: bold; + } + + .buy-btn { + width: 48rpx; + height: 48rpx; + border-radius: 50%; + background: linear-gradient(135deg, #ff4444, #ff6666); + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 4rpx 12rpx rgba(255, 68, 68, 0.3); + transition: all 0.3s; + } + + .buy-btn:active { + transform: scale(0.9); + } + + .buy-icon { + width: 26rpx; + height: 26rpx; + filter: brightness(0) invert(1); + } + + /* ===== 5. 空状态 ===== */ + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 0; + } + + .empty-image { + width: 200rpx; + height: 200rpx; + margin-bottom: 40rpx; + opacity: 0.7; + } + + .empty-text { + font-size: 30rpx; + color: #999; + margin-bottom: 40rpx; + } + + .empty-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + font-size: 28rpx; + padding: 20rpx 60rpx; + border-radius: 50rpx; + box-shadow: 0 8rpx 25rpx rgba(102, 126, 234, 0.3); + } + + /* ===== 6. 弹窗样式 ===== */ + .gonggao-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; + } + + .modal-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(5px); + } + + .modal-content { + width: 600rpx; + background: white; + border-radius: 30rpx; + overflow: hidden; + box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.2); + z-index: 1001; + animation: modalShow 0.3s; + } + + @keyframes modalShow { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } + } + + .modal-header { + padding: 40rpx; + border-bottom: 2rpx solid #f0f0f0; + display: flex; + justify-content: space-between; + align-items: center; + } + + .modal-title { + font-size: 32rpx; + font-weight: bold; + color: #333; + } + + .modal-close { + font-size: 40rpx; + color: #999; + width: 60rpx; + height: 60rpx; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + } + + .modal-close:active { + background: #f5f5f5; + } + + .modal-body { + max-height: 600rpx; + padding: 40rpx; + } + + .modal-text { + font-size: 28rpx; + color: #333; + line-height: 1.6; + white-space: normal; + word-break: break-word; + } + + /* ===== 7. 加载提示 ===== */ + .loading-toast { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(10px); + padding: 30rpx 50rpx; + border-radius: 25rpx; + display: flex; + flex-direction: column; + align-items: center; + z-index: 999; + } + + .loading-spinner { + width: 60rpx; + height: 60rpx; + border: 6rpx solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 20rpx; + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + .loading-text { + font-size: 26rpx; + color: white; + } \ No newline at end of file diff --git a/pages/jiedan/jiedan.js b/pages/jiedan/jiedan.js new file mode 100644 index 0000000..f0e4cbb --- /dev/null +++ b/pages/jiedan/jiedan.js @@ -0,0 +1,528 @@ +// pages/qiangdan/qiangdan.js +const app = getApp(); +import request from '../../utils/request.js'; +import PopupService from '../../services/popupService.js'; + +Page({ + data: { + // 1. 商品类型相关 + shangpinleixing: [], + xuanzhongLeixingId: null, + + // 2. 订单列表与分页相关 + dingdanList: [], + page: 1, + pageSize: 5, + hasMore: true, + isLoading: false, + + // 3. 全局状态字段 + dashoustatus: null, + zhuanghaoStatus: null, + dashouzhuangtai: null, + uid: null, + huiyuanList: [], + yajin: 0, + jifen: 0, + + // 4. 刷新控制字段 + scrollViewRefreshing: false, + lastRefreshTime: 0, + refreshCooldown: 2000, + isLoadingMore: false, + + // 5. 切换类型冷却 + lastSwitchTime: 0, + switchCooldown: 1500, + isSwitching: false, + + // 6. OSS地址 + ossImageUrl: app.globalData.ossImageUrl || '', + + // 🆕 7. 标签筛选相关 + bankuaiBiaoqian: [], // 当前商品类型所属板块的全部标签 + xuanzhongBiaoqianId: 0, // 0=全部 + }, + + async onLoad(options) { + this.loadGlobalStatus(); + await this.loadShangpinLeixing(); + PopupService.checkAndShow(this, 'jiedan'); + }, + + onHide: function () { + const popupComp = this.selectComponent('#popupNotice'); + if (popupComp && popupComp.cleanup) { + popupComp.cleanup(); + } + }, + + onShow() { + this.registerNotificationComponent(); + // 重新加载全局状态 + this.loadGlobalStatus(); + // 如果有选中类型则刷新订单 + if (this.data.xuanzhongLeixingId) { + this.loadDingdanList(true); + } + }, + + registerNotificationComponent() { + const notificationComp = this.selectComponent('#global-notification'); + if (notificationComp && notificationComp.showNotification) { + app.globalData.globalNotification = { + show: (data) => notificationComp.showNotification(data), + hide: () => notificationComp.hideNotification() + }; + } + }, + + loadGlobalStatus() { + const globalData = app.globalData; + this.setData({ + dashoustatus: wx.getStorageSync('dashoustatus'), + zhuanghaoStatus: globalData.zhanghaoStatus, + dashouzhuangtai: globalData.dashouzhuangtai, + uid: wx.getStorageSync('uid'), + huiyuanList: globalData.clumber || [], + yajin: globalData.yajin || 0, + jifen: globalData.jinfen || 0 + }); + }, + + // 加载商品类型 + async loadShangpinLeixing() { + wx.showLoading({ title: '加载商品类型...' }); + try { + const res = await request({ + url: '/dingdan/dsqdhqddlx', + method: 'POST', + header: { 'content-type': 'application/json' } + }); + if (res.statusCode === 200 && res.data) { + const data = res.data; + let list = []; + if (data.code === 200 || data.code === 0) { + if (data.data && Array.isArray(data.data.list)) list = data.data.list; + else if (data.data && Array.isArray(data.data)) list = data.data; + else if (Array.isArray(data.list)) list = data.list; + else if (Array.isArray(data)) list = data; + } + const processedList = this.processTupianUrl(list); + this.setData({ + shangpinleixing: processedList, + xuanzhongLeixingId: processedList[0]?.id || null + }); + if (this.data.xuanzhongLeixingId) { + this.loadDingdanList(true); + // 🆕 加载板块标签 + this.loadBankuaiBiaoqian(); + } + } else { + wx.showToast({ title: res.data?.msg || '加载商品类型失败', icon: 'none' }); + } + } catch (error) { + wx.showToast({ title: '网络错误,加载失败', icon: 'none' }); + } finally { + wx.hideLoading(); + } + }, + + // 处理图片URL + processTupianUrl(list) { + const ossUrl = app.globalData.ossImageUrl || ''; + return list.map(item => ({ + ...item, + full_tupian_url: item.tupian_url && !item.tupian_url.startsWith('http') + ? ossUrl + item.tupian_url + : (item.tupian_url || '/images/default-type.png') + })); + }, + + // 🆕 加载板块标签 + async loadBankuaiBiaoqian() { + const leixingId = this.data.xuanzhongLeixingId; + if (!leixingId) return; + try { + const res = await request({ + url: '/dengji/bkqhbq', + method: 'POST', + data: { leixing_id: leixingId } + }); + if (res && res.data.code === 0) { + this.setData({ + bankuaiBiaoqian: res.data.data.biaoqian_list || [], + xuanzhongBiaoqianId: 0 + }); + } + } catch (e) { + // 标签加载失败不影响主流程 + } + }, + + // 选择商品类型 + async selectLeixing(e) { + const now = Date.now(); + const lastSwitchTime = this.data.lastSwitchTime; + const switchCooldown = this.data.switchCooldown; + + if (now - lastSwitchTime < switchCooldown) { + wx.showToast({ + title: `操作太快,请${Math.ceil((switchCooldown - (now - lastSwitchTime)) / 1000)}秒后再试`, + icon: 'none', + duration: 1500 + }); + return; + } + if (this.data.isSwitching) return; + + const leixingId = e.currentTarget.dataset.id; + if (leixingId === this.data.xuanzhongLeixingId) return; + + this.setData({ isSwitching: true, lastSwitchTime: now }); + wx.showLoading({ title: '切换中...' }); + + try { + this.setData({ + xuanzhongLeixingId: leixingId, + dingdanList: [], + page: 1, + hasMore: true, + xuanzhongBiaoqianId: 0 // 重置标签筛选 + }); + await this.loadDingdanList(true); + // 🆕 切换类型后加载新标签 + await this.loadBankuaiBiaoqian(); + } catch (error) { + console.error('切换类型失败:', error); + } finally { + this.setData({ isSwitching: false }); + wx.hideLoading(); + } + }, + + // 🆕 选择标签筛选 + async selectBiaoqian(e) { + const id = e.currentTarget.dataset.id; + if (id === this.data.xuanzhongBiaoqianId) return; + this.setData({ + xuanzhongBiaoqianId: id, + dingdanList: [], + page: 1, + hasMore: true + }); + await this.loadDingdanList(true); + }, + + // 加载订单列表 + async loadDingdanList(isRefresh = false) { + if (this.data.isLoading || !this.data.xuanzhongLeixingId) return; + + const loadPage = isRefresh ? 1 : this.data.page; + if (!isRefresh && !this.data.hasMore) return; + + this.setData({ + isLoading: true, + isLoadingMore: !isRefresh + }); + + try { + const res = await request({ + url: '/dingdan/ddhq', + method: 'POST', + data: { + leixing_id: this.data.xuanzhongLeixingId, + page: loadPage, + page_size: this.data.pageSize, + // 🆕 传递标签筛选参数(0时不传) + biaoqian_id: this.data.xuanzhongBiaoqianId || undefined + } + }); + + this.setData({ + isLoading: false, + isLoadingMore: false, + scrollViewRefreshing: false + }); + + if (res.data.code === 200 || res.data.code === 0) { + const newList = res.data.data.list || []; + const hasMore = res.data.data.has_more || false; + + const processedList = newList.map(item => this.processDingdanItem(item)); + const updatedList = isRefresh ? processedList : [...this.data.dingdanList, ...processedList]; + updatedList.sort((a, b) => (b.creat_time || '').localeCompare(a.creat_time || '')); + + this.setData({ + dingdanList: updatedList, + page: loadPage, + hasMore: hasMore + }); + + if (isRefresh) { + this.setData({ lastRefreshTime: Date.now() }); + } + } else { + wx.showToast({ title: res.data.msg || '加载失败', icon: 'none' }); + } + } catch (error) { + console.error('加载订单失败:', error); + this.setData({ + isLoading: false, + isLoadingMore: false, + scrollViewRefreshing: false + }); + wx.showToast({ title: '网络请求失败', icon: 'none' }); + } + }, + + // 处理单条订单数据(包含标签) + processDingdanItem(item) { + const ossUrl = app.globalData.ossImageUrl || ''; + let fullTupianUrl = ''; + + if (item.tupian) { + fullTupianUrl = !item.tupian.startsWith('http') ? ossUrl + item.tupian : item.tupian; + } else { + const leixing = this.data.shangpinleixing.find(l => l.id === item.leixing_id); + fullTupianUrl = leixing ? leixing.full_tupian_url : '/images/default-order.png'; + } + + const isZhiding = item.zhuangtai === 7; + + let zhidingAvatar = ''; + if (item.zhiding_avatar) { + zhidingAvatar = !item.zhiding_avatar.startsWith('http') + ? ossUrl + item.zhiding_avatar + : item.zhiding_avatar; + } + const zhidingNicheng = item.zhiding_nicheng || ''; + + return { + ...item, + full_tupian_url: fullTupianUrl, + isZhiding: isZhiding, + isPingtai: item.pingtai == 1, + isShangjia: item.pingtai == 2, + zhiding_avatar_full: zhidingAvatar, + zhiding_nicheng: zhidingNicheng, + // 🆕 三层标签(后端返回,直接保留) + xuqiu_biaoqian: item.xuqiu_biaoqian || [], + dashou_biaoqian: item.dashou_biaoqian || [], + shangjia_biaoqian: item.shangjia_biaoqian || [], + }; + }, + + // 跳转到充值页面(原封不动) + goToChongzhiPage(failureType, huiyuanId = null, requiredYajin = 0) { + let url = '/pages/dashouchongzhi/dashouchongzhi'; + let params = {}; + + switch (failureType) { + case 'huiyuan': + params = { needScroll: '0', scrollTo: 'member' }; + break; + case 'yajin': + params = { needScroll: '1', scrollTo: 'bottom', requiredYajin: requiredYajin }; + break; + case 'jifen': + params = { needScroll: '1', scrollTo: 'bottom' }; + break; + } + + if (huiyuanId) params.huiyuanId = huiyuanId; + + const queryString = Object.keys(params) + .map(key => `${key}=${encodeURIComponent(params[key])}`) + .join('&'); + + wx.navigateTo({ + url: `${url}?${queryString}`, + fail: (err) => { + console.error('跳转失败:', err); + wx.showToast({ title: '跳转失败,请稍后重试', icon: 'none' }); + } + }); + }, + + // 抢单按钮(原封不动) + async onQiangdanTap(e) { + const dingdanItem = e.currentTarget.dataset.item; + if (!dingdanItem) return; + + // 校验1: 打手身份 + if (!this.data.dashoustatus || this.data.dashoustatus != 1) { + wx.showToast({ title: '请先开启打手身份', icon: 'none' }); + return; + } + // 校验2: 账号状态 + if (this.data.zhuanghaoStatus != 1) { + wx.showToast({ title: '账号异常,无法接单', icon: 'none' }); + return; + } + // 校验3: 打手接单状态 + if (this.data.dashouzhuangtai != 1) { + wx.showToast({ title: '您有订单正在接待中', icon: 'none' }); + return; + } + // 校验4: 指定订单 + if (dingdanItem.isZhiding) { + if (!this.data.uid || this.data.uid != dingdanItem.zhiding_uid) { + wx.showToast({ title: '此订单为指定单', icon: 'none' }); + return; + } + } + // 校验5: 会员要求 + if (dingdanItem.yaoqiuleixing == 1) { + const hasHuiyuan = this.data.huiyuanList.some(h => h.huiyuanid == dingdanItem.huiyuan_id); + if (!hasHuiyuan) { + wx.showToast({ + title: '未开通对应会员,无法抢单', + icon: 'none', + duration: 2000 + }); + setTimeout(() => { + this.goToChongzhiPage('huiyuan', dingdanItem.huiyuan_id); + }, 500); + return; + } + } + // 校验6: 押金要求 + if (dingdanItem.yaoqiuleixing == 2) { + if (this.data.yajin < dingdanItem.yajin) { + wx.showToast({ + title: `押金不足,需${dingdanItem.yajin}`, + icon: 'none', + duration: 2000 + }); + setTimeout(() => { + this.goToChongzhiPage('yajin', null, dingdanItem.yajin); + }, 500); + return; + } + } + // 校验7: 积分要求 + if (this.data.jifen < 5) { + wx.showToast({ + title: '积分不足,至少需要5积分', + icon: 'none', + duration: 2000 + }); + setTimeout(() => { + this.goToChongzhiPage('jifen'); + }, 500); + return; + } + + // 确认框 + const that = this; + wx.showModal({ + title: '确认抢单', + content: `确定要抢此订单吗?${dingdanItem.isZhiding ? '(指定订单)' : ''}`, + success: async function (res) { + if (res.confirm) { + wx.showLoading({ title: '抢单中...', mask: true }); + try { + const qiangdanRes = await request({ + url: '/dingdan/qiangdan', + method: 'POST', + data: { dingdan_id: dingdanItem.dingdan_id } + }); + wx.hideLoading(); + + if (qiangdanRes.data.code === 200 || qiangdanRes.data.code === 0) { + that.showQiangdanSuccessEffect(); + if (app.globalData) { + app.globalData.dashouzhuangtai = 0; + that.setData({ dashouzhuangtai: 0 }); + } + const newList = that.data.dingdanList.filter( + item => item.dingdan_id !== dingdanItem.dingdan_id + ); + that.setData({ dingdanList: newList }); + } else { + wx.showToast({ + title: qiangdanRes.data.msg || '抢单失败', + icon: 'none' + }); + } + } catch (error) { + wx.hideLoading(); + console.error('抢单请求失败:', error); + wx.showToast({ title: '网络错误,抢单失败', icon: 'none' }); + } + } + } + }); + }, + + showQiangdanSuccessEffect() { + wx.showToast({ title: '抢单成功!', icon: 'success', duration: 2000 }); + }, + + onViewDetail(e) { + const { type, content } = e.currentTarget.dataset; + wx.showModal({ + title: type === 'jieshao' ? '订单介绍' : '订单备注', + content: content, + showCancel: false, + confirmText: '知道了' + }); + }, + + onReachBottom() { + const now = Date.now(); + const lastTime = this.data.lastRefreshTime; + const cooldown = this.data.refreshCooldown; + + if (now - lastTime < cooldown) { + wx.showToast({ + title: `操作太快,请${Math.ceil((cooldown - (now - lastTime)) / 1000)}秒后再试`, + icon: 'none', + duration: 1500 + }); + return; + } + + if (this.data.isLoading || this.data.isLoadingMore) return; + + if (this.data.hasMore && !this.data.isLoading && this.data.xuanzhongLeixingId) { + this.setData({ page: this.data.page + 1 }); + this.loadDingdanList(false); + } + }, + + onPullDownRefresh() { + const now = Date.now(); + const lastTime = this.data.lastRefreshTime; + const cooldown = this.data.refreshCooldown; + + if (now - lastTime < cooldown) { + this.setData({ scrollViewRefreshing: false }); + wx.showToast({ + title: `操作太快,请${Math.ceil((cooldown - (now - lastTime)) / 1000)}秒后再试`, + icon: 'none', + duration: 1500 + }); + return; + } + + if (this.data.isLoading) { + this.setData({ scrollViewRefreshing: false }); + return; + } + + this.setData({ + scrollViewRefreshing: true, + lastRefreshTime: now, + page: 1, + hasMore: true + }); + + if (this.data.xuanzhongLeixingId) { + this.loadDingdanList(true); + } else { + this.setData({ scrollViewRefreshing: false }); + } + } +}); \ No newline at end of file diff --git a/pages/jiedan/jiedan.json b/pages/jiedan/jiedan.json new file mode 100644 index 0000000..eee9b81 --- /dev/null +++ b/pages/jiedan/jiedan.json @@ -0,0 +1,12 @@ +{ + "navigationBarTitleText": "抢单大厅", + "navigationBarBackgroundColor": "#0b0f1a", + "navigationBarTextStyle": "white", + "enablePullDownRefresh": false, + "backgroundTextStyle": "dark", + "backgroundColor": "#0b0f1a", + "usingComponents": { + "chenghao-tag": "/components/chenghao-tag/chenghao-tag", + "global-notification": "/components/global-notification/global-notification" + } + } \ No newline at end of file diff --git a/pages/jiedan/jiedan.wxml b/pages/jiedan/jiedan.wxml new file mode 100644 index 0000000..6450749 --- /dev/null +++ b/pages/jiedan/jiedan.wxml @@ -0,0 +1,208 @@ + + + + + + + + + + + {{item.jieshao}} + + + + + + + + + + + + 全部 + + + + + + + + + + + + + + + + + ⚡️ 正在刷新... + ▼ 下拉刷新订单 + + + + + + 请先选择上方订单类型 + + + + 当前类型暂无待抢订单 + + + + + + + {{item.creat_time}} + + ⚙️ 平台订单 + + + + + 指定 + + {{item.zhiding_nicheng || '指定打手'}} + UID: {{item.zhiding_uid || '未知'}} + + + + + + + + + + 介绍: + {{item.jieshao || '暂无介绍'}} + [点击查看] + + + + + 备注: + {{item.beizhu}} + [点击查看] + + + + + 需求标签: + + + + + + + + 💰 + 分佣 + {{item.dashou_fencheng || 0}} + + + + ⚡ 立即抢单 + + + + + + + + + + + {{item.creat_time}} + + 🏪 商家发单 + + + + + 指定 + + {{item.zhiding_nicheng || '指定打手'}} + UID: {{item.zhiding_uid || '未知'}} + + + + + + + + + {{item.sjnicheng || '未知商家'}} + + + + + + + + 订单介绍: + {{item.jieshao || '暂无介绍'}} + [点击查看] + + + + 商家备注: + {{item.beizhu}} + [点击查看] + + + + + 需求标签: + + + + + + + + 💰 + 分佣 + {{item.dashou_fencheng || 0}} + + + + ⚡ 立即抢单 + + + + + + + + + ⚙️ 加载更多订单中... + —— 没有更多订单了 —— + + + + + + ⚡ 订单加载中... + + + + + + + \ No newline at end of file diff --git a/pages/jiedan/jiedan.wxss b/pages/jiedan/jiedan.wxss new file mode 100644 index 0000000..52ed1a3 --- /dev/null +++ b/pages/jiedan/jiedan.wxss @@ -0,0 +1,762 @@ +/* pages/qiangdan/qiangdan.wxss - 高级机甲风格重构版(对称优化) */ +.qiangdan-page { + height: 100vh; + display: flex; + flex-direction: column; + background: radial-gradient(ellipse at top, #1a1f30, #0b0f1a); + overflow: hidden; + position: relative; + } + + /* 背景动态纹理,增加层次感 */ + .qiangdan-page::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + linear-gradient(45deg, rgba(0, 247, 255, 0.02) 0%, transparent 30%), + radial-gradient(circle at 30% 50%, rgba(255, 77, 109, 0.03) 0%, transparent 50%), + repeating-linear-gradient(45deg, rgba(255,255,255,0.01) 0px, rgba(255,255,255,0.01) 1px, transparent 1px, transparent 6px); + pointer-events: none; + z-index: 0; + } + + /* ====== 1. 商品类型区域 ====== */ + .leixing-quyu { + padding: 30rpx 0 20rpx; + background: rgba(18, 22, 38, 0.9); + backdrop-filter: blur(20rpx); + border-bottom: 1rpx solid rgba(255, 255, 255, 0.05); + position: relative; + z-index: 10; + flex-shrink: 0; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.5); + } + + .leixing-scroll { + width: 100%; + white-space: nowrap; + height: 180rpx; + } + + .leixing-container { + display: inline-flex; + padding: 0 30rpx; + gap: 20rpx; + } + + .leixing-item { + display: inline-flex; + flex-direction: column; + align-items: center; + width: 160rpx; + position: relative; + transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); + } + + .leixing-active { + transform: translateY(-8rpx); + } + + .guangyun-effect { + position: absolute; + top: -15rpx; + left: -15rpx; + right: -15rpx; + bottom: -15rpx; + background: radial-gradient(circle at center, rgba(0, 200, 255, 0.3) 0%, transparent 70%); + border-radius: 50rpx; + z-index: 0; + animation: guangyun-pulse 2s infinite ease-in-out; + } + + @keyframes guangyun-pulse { + 0%, 100% { opacity: 0.5; transform: scale(1); } + 50% { opacity: 1; transform: scale(1.1); } + } + + .leixing-tupian { + width: 120rpx; + height: 120rpx; + border-radius: 32rpx; + z-index: 1; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.6); + transition: all 0.3s ease; + border: 2rpx solid rgba(0, 200, 255, 0.4); + } + + .leixing-active .leixing-tupian { + box-shadow: 0 16rpx 40rpx rgba(0, 200, 255, 0.5); + border: 2rpx solid #00c8ff; + } + + .leixing-jieshao { + margin-top: 16rpx; + font-size: 24rpx; + color: #b0c0e0; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: center; + z-index: 1; + font-weight: 500; + } + + .leixing-active .leixing-jieshao { + color: #00c8ff; + text-shadow: 0 0 8px rgba(0,200,255,0.5); + } + + /* ====== 2. 分割线 ====== */ + .fenge-xian { + height: 2rpx; + background: linear-gradient(90deg, transparent, #00c8ff, #ff4d6d, transparent); + flex-shrink: 0; + margin: 0 30rpx; + opacity: 0.5; + } + + /* ====== 3. 订单列表容器 ====== */ + .dingdan-container { + flex: 1; + overflow: hidden; + position: relative; + z-index: 1; + padding: 0 20rpx; /* 统一左右内边距 */ + } + + .dingdan-scroll-view { + height: 100%; + -webkit-overflow-scrolling: touch; + } + + .refresher-container { + height: 100rpx; + display: flex; + align-items: center; + justify-content: center; + } + + .refreshing-text, + .pull-down-text { + font-size: 26rpx; + color: #6a9cff; + text-shadow: 0 0 8px rgba(0,200,255,0.3); + } + + /* ====== 4. 空状态提示 ====== */ + .empty-tip { + padding: 160rpx 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + .tip-icon { + width: 180rpx; + height: 180rpx; + opacity: 0.5; + margin-bottom: 30rpx; + filter: drop-shadow(0 0 20px rgba(0,200,255,0.3)); + } + + .tip-text { + font-size: 28rpx; + color: #8a9ac0; + letter-spacing: 2rpx; + } + + /* ====== 5. 订单卡片通用样式(移除左右边框,改用顶部标识)====== */ + .dingdan-card { + margin: 20rpx 0; /* 左右由容器控制 */ + border-radius: 36rpx; + padding: 28rpx 24rpx; + position: relative; + overflow: hidden; + box-shadow: 0 20rpx 40rpx rgba(0, 0, 0, 0.6); + transition: all 0.3s ease; + border: 1rpx solid rgba(255, 255, 255, 0.03); + backdrop-filter: blur(30rpx); + } + + .dingdan-card:active { + transform: scale(0.998); + box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.8); + } + + /* 平台订单 - 冷色调,顶部光效 */ + .pingtai-card { + background: linear-gradient(145deg, rgba(18, 28, 48, 0.95), rgba(8, 18, 35, 0.98)); + } + + .pingtai-card::before { + content: ''; + position: absolute; + top: 0; + left: 10%; + width: 80%; + height: 4rpx; + background: linear-gradient(90deg, transparent, #00c8ff, transparent); + border-radius: 2rpx; + opacity: 0.7; + } + + /* 商家订单 - 暖色调,顶部光效 */ + .shangjia-card { + background: linear-gradient(145deg, rgba(38, 18, 48, 0.95), rgba(25, 8, 35, 0.98)); + } + + .shangjia-card::before { + content: ''; + position: absolute; + top: 0; + left: 10%; + width: 80%; + height: 4rpx; + background: linear-gradient(90deg, transparent, #ff4d6d, transparent); + border-radius: 2rpx; + opacity: 0.7; + } + + /* 卡片顶部 */ + .card-top { + display: flex; + align-items: center; + margin-bottom: 20rpx; + padding-bottom: 16rpx; + border-bottom: 1rpx dashed rgba(255, 255, 255, 0.08); + } + + .creat-time { + font-size: 26rpx; + color: #c0d0ff; + font-weight: 500; + text-shadow: 0 0 5px rgba(0,200,255,0.3); + } + + .top-line { + flex: 1; + height: 1rpx; + background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.2), transparent); + margin: 0 20rpx; + } + + .pingtai-tag { + font-size: 24rpx; + color: #aac8ff; + padding: 6rpx 18rpx; + background: rgba(0,200,255,0.08); + border-radius: 40rpx; + border: 1rpx solid rgba(0,200,255,0.3); + box-shadow: 0 0 10rpx rgba(0,200,255,0.2); + } + + /* ====== 6. 指定单标识行(新设计)====== */ + .zhiding-row { + display: flex; + align-items: center; + margin-bottom: 24rpx; + padding: 12rpx 20rpx; + background: rgba(255, 77, 109, 0.1); + border-radius: 60rpx; + border: 1rpx solid rgba(255, 77, 109, 0.3); + box-shadow: 0 0 20rpx rgba(255, 77, 109, 0.2); + backdrop-filter: blur(10rpx); + gap: 12rpx; + } + + .zhiding-icon { + font-size: 28rpx; + color: #ff4d6d; + font-weight: 700; + background: rgba(255, 77, 109, 0.2); + padding: 4rpx 16rpx; + border-radius: 40rpx; + text-shadow: 0 0 8px #ff4d6d; + letter-spacing: 2rpx; + } + + .zhiding-avatar { + width: 48rpx; + height: 48rpx; + border-radius: 50%; + border: 2rpx solid #ff4d6d; + box-shadow: 0 0 15rpx #ff4d6d; + object-fit: cover; + } + + .zhiding-nicheng { + font-size: 26rpx; + color: #ffc0d0; + font-weight: 500; + max-width: 160rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .zhiding-uid { + font-size: 24rpx; + color: #ff9aab; + flex: 1; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: monospace; + } + + /* ====== 7. 平台订单内容区 ====== */ + .card-content { + display: flex; + margin-bottom: 24rpx; + align-items: center; + } + + .neirong-tupian { + width: 140rpx; + height: 140rpx; + border-radius: 50%; + margin-right: 24rpx; + flex-shrink: 0; + box-shadow: 0 10rpx 30rpx rgba(0,200,255,0.3); + border: 2rpx solid #00c8ff; + object-fit: cover; + } + + .jieshao-box { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 140rpx; + padding: 8rpx 0; + } + + .jieshao-label { + font-size: 26rpx; + color: #d0e0ff; + font-weight: 600; + margin-bottom: 6rpx; + text-shadow: 0 0 5px rgba(0,200,255,0.3); + } + + .jieshao-text { + font-size: 26rpx; + color: #b0c8f0; + line-height: 1.4; + word-break: break-word; + } + + .jieshao-ellipsis .jieshao-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + text-overflow: ellipsis; + } + + /* ====== 8. 商家订单特有样式 ====== */ + .shangjia-info { + display: flex; + align-items: center; + margin-bottom: 20rpx; + } + + .shangjia-touxiang { + width: 90rpx; + height: 90rpx; + border-radius: 50%; + margin-right: 20rpx; + border: 2rpx solid #ff4d6d; + box-shadow: 0 6rpx 18rpx rgba(255, 77, 109, 0.5); + object-fit: cover; + } + + .shangjia-nicheng { + font-size: 30rpx; + color: #ffb3c6; + font-weight: 600; + text-shadow: 0 0 8px rgba(255, 77, 109, 0.4); + } + + .shangjia-jieshao, + .shangjia-beizhu { + margin-bottom: 16rpx; + padding: 14rpx 18rpx; + background: rgba(255, 255, 255, 0.02); + border-radius: 24rpx; + border: 1rpx solid rgba(255, 77, 109, 0.2); + } + + .shangjia-jieshao .jieshao-label, + .shangjia-beizhu .beizhu-label { + display: block; + margin-bottom: 6rpx; + font-size: 24rpx; + color: #ff9aab; + } + + .shangjia-jieshao .jieshao-text, + .shangjia-beizhu .beizhu-text { + font-size: 24rpx; + color: #e0c8ff; + line-height: 1.4; + } + + .shangjia-jieshao.jieshao-ellipsis .jieshao-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + } + + .shangjia-beizhu.beizhu-ellipsis .beizhu-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + } + + /* ====== 9. 通用备注区 ====== */ + .beizhu-box { + margin-bottom: 20rpx; + padding: 14rpx 18rpx; + background: rgba(255, 255, 255, 0.02); + border-radius: 24rpx; + border: 1rpx solid rgba(0,200,255,0.2); + } + + .beizhu-label { + font-size: 24rpx; + color: #80d0ff; + font-weight: 600; + margin-bottom: 6rpx; + display: block; + } + + .beizhu-text { + font-size: 24rpx; + color: #b0c8f0; + line-height: 1.4; + } + + .beizhu-ellipsis .beizhu-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + } + + .view-more { + display: block; + font-size: 22rpx; + color: #00c8ff; + margin-top: 6rpx; + text-align: right; + opacity: 0.8; + text-shadow: 0 0 5px #00c8ff; + } + + /* ====== 10. 卡片底部:分佣 + 按钮 ====== */ + .card-bottom { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 20rpx; + border-top: 1rpx solid rgba(255, 255, 255, 0.06); + } + + /* 分佣容器(图标占位,用户可自定义) */ + .fenyong-box { + display: flex; + align-items: baseline; + background: rgba(0, 0, 0, 0.3); + padding: 8rpx 20rpx; + border-radius: 60rpx; + border: 1rpx solid rgba(255, 215, 0, 0.3); + } + + /* 分佣图标占位符,用户可替换为自己图标 */ + .fenyong-icon { + font-size: 32rpx; + margin-right: 10rpx; + filter: drop-shadow(0 0 8px gold); + } + + .fenyong-text { + font-size: 24rpx; + color: #c0b0a0; + margin-right: 8rpx; + } + + .fenyong-price { + font-size: 44rpx; + color: #ffd700; + font-weight: 800; + margin-right: 4rpx; + text-shadow: 0 0 20px rgba(255, 215, 0, 0.8); + } + + .fenyong-unit { + font-size: 24rpx; + color: #a0a0b0; + } + + /* ====== 11. 机甲风格抢单按钮(对称优化)====== */ + .qiangdan-btn { + position: relative; + padding: 16rpx 44rpx; + border: none; + overflow: hidden; + transition: all 0.2s ease; + cursor: pointer; + z-index: 1; + /* 多边形形状,保证上下对称 */ + clip-path: polygon(10% 0%, 90% 0%, 100% 25%, 100% 75%, 90% 100%, 10% 100%, 0% 75%, 0% 25%); + } + + /* 按钮统一基础样式,无上下不对称阴影 */ + .mecha-btn { + background: linear-gradient(145deg, #2a3050, #181e38); + box-shadow: + 0 8rpx 0 #0a0a1a, + 0 12rpx 28rpx rgba(0, 0, 0, 0.7), + inset 0 2rpx 6rpx rgba(255, 255, 255, 0.2), + inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.3); + border: 1rpx solid rgba(255, 255, 255, 0.1); + } + + /* 平台按钮冷光 */ + .pingtai-btn.mecha-btn { + background: linear-gradient(145deg, #1a2a48, #0e1a30); + box-shadow: + 0 8rpx 0 #0a1525, + 0 12rpx 28rpx rgba(0, 0, 0, 0.7), + inset 0 2rpx 6rpx #00c8ff, + inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.3); + } + + /* 商家按钮暖光 */ + .shangjia-btn.mecha-btn { + background: linear-gradient(145deg, #3a2048, #251030); + box-shadow: + 0 8rpx 0 #2a0a1a, + 0 12rpx 28rpx rgba(0, 0, 0, 0.7), + inset 0 2rpx 6rpx #ff4d6d, + inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.3); + } + + .btn-text { + font-size: 32rpx; + color: #fff; + font-weight: 700; + letter-spacing: 3rpx; + position: relative; + z-index: 3; + text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.7), 0 0 15rpx currentColor; + } + + /* 扫光效果 */ + .btn-shine { + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient( + to right, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.25) 50%, + rgba(255, 255, 255, 0) 100% + ); + transform: rotate(30deg); + animation: btn-shimmer 2.8s infinite linear; + z-index: 2; + } + + /* 内发光,只在悬停/按下时出现,保证对称 */ + .btn-glow { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.5), transparent 70%); + opacity: 0; + transition: opacity 0.3s; + z-index: 1; + } + + .mecha-btn:hover .btn-glow, + .mecha-btn:active .btn-glow { + opacity: 0.3; + } + + /* 按下效果,保持对称 */ + .mecha-btn:active { + transform: translateY(4rpx) scale(0.98); + box-shadow: + 0 4rpx 0 #0a0a1a, + 0 8rpx 20rpx rgba(0, 0, 0, 0.7), + inset 0 2rpx 6rpx rgba(255, 255, 255, 0.2), + inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.3); + } + + /* 机械纹理,保持视觉丰富但不过分 */ + .mecha-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.02) 0px, rgba(255, 255, 255, 0.02) 2px, transparent 2px, transparent 8px); + pointer-events: none; + z-index: 1; + opacity: 0.5; + } + + @keyframes btn-shimmer { + 0% { transform: translateX(-100%) rotate(30deg); } + 100% { transform: translateX(100%) rotate(30deg); } + } + + /* ====== 12. 加载状态 ====== */ + .loading-more, + .no-more { + padding: 40rpx 0; + text-align: center; + } + + .loading-text { + font-size: 26rpx; + color: #6a9cff; + text-shadow: 0 0 10px rgba(0,200,255,0.5); + animation: pulse 1.5s infinite; + } + + .no-more-text { + font-size: 26rpx; + color: #4a5a8a; + letter-spacing: 3rpx; + } + + @keyframes pulse { + 0%, 100% { opacity: 0.7; } + 50% { opacity: 1; } + } + + .loading-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(5, 8, 18, 0.95); + backdrop-filter: blur(30rpx); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 9999; + } + + .loading-spinner { + width: 90rpx; + height: 90rpx; + border: 6rpx solid rgba(0,200,255,0.2); + border-top: 6rpx solid #00c8ff; + border-right: 6rpx solid #ff4d6d; + border-radius: 50%; + animation: spin 1.2s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite; + margin-bottom: 30rpx; + box-shadow: 0 0 50rpx #00c8ff; + } + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + .loading-mask-text { + font-size: 30rpx; + color: #c0d0ff; + text-shadow: 0 0 15px rgba(0,200,255,0.7); + letter-spacing: 4rpx; + } + + + + + +/* 🆕 标签筛选区 */ +.biaoqian-quyu { + padding: 12rpx 20rpx; + background: rgba(18, 22, 38, 0.7); + backdrop-filter: blur(10rpx); + } + .biaoqian-scroll { + white-space: nowrap; + height: 80rpx; + } + .biaoqian-container { + display: inline-flex; + gap: 16rpx; + padding-right: 20rpx; + } + .biaoqian-item { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 12rpx; + border-radius: 20rpx; + background: rgba(255,255,255,0.05); + transition: all 0.2s; + } + .biaoqian-active { + background: rgba(0,200,255,0.2); + border: 1rpx solid #00c8ff; + } + .quanbu-text { + font-size: 26rpx; + color: #ccc; + padding: 0 12rpx; + } + + /* 订单卡片内标签滚动 */ + .xuqiu-biaoqian-area { + display: flex; + align-items: center; + margin-top: 16rpx; + padding: 8rpx 0; + } + .xuqiu-label { + font-size: 24rpx; + color: #80d0ff; + margin-right: 12rpx; + flex-shrink: 0; + } + .xuqiu-biaoqian-scroll { + flex: 1; + white-space: nowrap; + } + .biaoqian-inline-scroll { + flex: 1; + white-space: nowrap; + margin-left: 10rpx; + } + /* 调整指定打手行内的标签滚动 */ + .zhiding-row .biaoqian-inline-scroll { + margin-left: auto; + max-width: 50%; + } + diff --git a/pages/jiedanchi/jiedanchi.js b/pages/jiedanchi/jiedanchi.js new file mode 100644 index 0000000..a081b4e --- /dev/null +++ b/pages/jiedanchi/jiedanchi.js @@ -0,0 +1,467 @@ +// pages/jiedanchi/jiedanchi.js +const app = getApp(); +import request from '../../utils/request.js'; +import PopupService from '../../services/popupService.js'; + +Page({ + data: { + // 1. 商品类型相关 + shangpinleixing: [], // 商品类型列表 {id, jieshao, tupian_url} + xuanzhongLeixingId: null, // 当前选中的类型ID + + // 2. 订单列表与分页相关 + dingdanList: [], // 当前展示的订单列表 + page: 1, + pageSize: 10, + hasMore: true, + isLoading: false, + + // 3. 全局状态字段 + dashoustatus: null, + zhuanghaoStatus: null, // 注意变量名:zhuanghaoStatus + dashouzhuangtai: null, + uid: null, + huiyuanList: [], // clumber 列表 + yajin: 0, + jifen: 0, + + // 4. 刷新控制字段 + scrollViewRefreshing: false, + lastRefreshTime: 0, + refreshCooldown: 2000, + isLoadingMore: false, + + // 5. 切换类型冷却时间控制 + lastSwitchTime: 0, + switchCooldown: 1500, + isSwitching: false, + + // 6. 全局OSS地址 + ossImageUrl: app.globalData.ossImageUrl || '', + + // 7. 授权状态控制 + showUnauthorized: false, + unauthorizedMsg: '您尚未开启此功能', + }, + + async onLoad(options) { + this.loadGlobalStatus(); + this.checkAuthorization(); + + }, + // 🆕 页面隐藏时清理弹窗视图 + onHide: function () { + const popupComp = this.selectComponent('#popupNotice'); + if (popupComp && popupComp.cleanup) { + popupComp.cleanup(); + } +}, + + onShow() { + this.registerNotificationComponent(); + this.loadGlobalStatus(); + this.checkAuthorization(); + }, + + registerNotificationComponent() { + const notificationComp = this.selectComponent('#global-notification'); + if (notificationComp && notificationComp.showNotification) { + app.globalData.globalNotification = { + show: (data) => notificationComp.showNotification(data), + hide: () => notificationComp.hideNotification() + }; + } + }, + + loadGlobalStatus() { + const globalData = app.globalData; + // 从缓存读取,确保与抢单时使用的数据源一致 + const dashoustatusCache = wx.getStorageSync('dashoustatus'); + const dashoustatus = dashoustatusCache !== '' ? dashoustatusCache : null; + + // 从 globalData 或缓存获取 zhuanghaoStatus(注意字段名可能有差异) + let zhuanghaoStatus = globalData.zhanghaoStatus; + if (zhuanghaoStatus === undefined || zhuanghaoStatus === null) { + zhuanghaoStatus = wx.getStorageSync('zhanghaoStatus'); + } + + this.setData({ + dashoustatus: dashoustatus, + zhuanghaoStatus: zhuanghaoStatus, + dashouzhuangtai: globalData.dashouzhuangtai, + uid: wx.getStorageSync('uid'), + huiyuanList: globalData.clumber || [], + yajin: globalData.yajin || 0, + jifen: globalData.jinfen || 0 + }); + + // 打印当前值,方便调试 + //console.log('loadGlobalStatus -> dashoustatus:', dashoustatus, 'zhuanghaoStatus:', zhuanghaoStatus); + }, + + checkAuthorization() { + const { dashoustatus, zhuanghaoStatus } = this.data; + //console.log('checkAuthorization -> dashoustatus:', dashoustatus, 'zhuanghaoStatus:', zhuanghaoStatus); + + // 完全复制抢单时的判断逻辑 + const isDashouValid = (dashoustatus && dashoustatus == 1) || false; // 存在且等于1 + const isZhuanghaoValid = (zhuanghaoStatus == 1); // 宽松比较,null/undefined/0 均视为 false + + const isAuthorized = isDashouValid && isZhuanghaoValid; + + if (!isAuthorized) { + this.setData({ showUnauthorized: true }); + let msg = ''; + if (!isDashouValid) msg = '您尚未开启身份'; + //else if (!isZhuanghaoValid) msg = '您的账号状态异常'; + this.setData({ unauthorizedMsg: msg || '无法使用抢单功能' }); + } else { + this.setData({ showUnauthorized: false }); + if (!this.data.shangpinleixing.length) { + this.loadShangpinLeixing(); + } else if (this.data.xuanzhongLeixingId) { + this.loadDingdanList(true); + } + PopupService.checkAndShow(this, 'jiedanchi'); + } + }, + + async loadShangpinLeixing() { + wx.showLoading({ title: '加载商品类型...' }); + try { + const res = await request({ + url: '/dingdan/dsqdhqddlx', + method: 'POST', + header: { 'content-type': 'application/json' } + }); + + if (res.statusCode === 200 && res.data) { + const data = res.data; + let list = []; + if (data.code === 200 || data.code === 0) { + if (data.data && Array.isArray(data.data.list)) { + list = data.data.list; + } else if (data.data && Array.isArray(data.data)) { + list = data.data; + } else if (Array.isArray(data.list)) { + list = data.list; + } else if (Array.isArray(data)) { + list = data; + } + } + const processedList = this.processTupianUrl(list); + this.setData({ + shangpinleixing: processedList, + xuanzhongLeixingId: processedList[0]?.id || null + }); + if (this.data.xuanzhongLeixingId) { + this.loadDingdanList(true); + } + } else { + wx.showToast({ title: res.data?.msg || '加载商品类型失败', icon: 'none', duration: 2000 }); + } + } catch (error) { + console.error('加载商品类型失败:', error); + wx.showToast({ title: '网络错误,加载失败', icon: 'none', duration: 2000 }); + } finally { + wx.hideLoading(); + } + }, + + processTupianUrl(list) { + const ossUrl = app.globalData.ossImageUrl || ''; + return list.map(item => ({ + ...item, + full_tupian_url: item.tupian_url && !item.tupian_url.startsWith('http') ? ossUrl + item.tupian_url : (item.tupian_url || '/images/default-type.png') + })); + }, + + async selectLeixing(e) { + const now = Date.now(); + if (now - this.data.lastSwitchTime < this.data.switchCooldown) { + wx.showToast({ + title: `操作太快,请${Math.ceil((this.data.switchCooldown - (now - this.data.lastSwitchTime)) / 1000)}秒后再试`, + icon: 'none', + duration: 1500 + }); + return; + } + if (this.data.isSwitching) return; + const leixingId = e.currentTarget.dataset.id; + if (leixingId === this.data.xuanzhongLeixingId) return; + this.setData({ isSwitching: true, lastSwitchTime: now }); + wx.showLoading({ title: '切换中...' }); + try { + this.setData({ + xuanzhongLeixingId: leixingId, + dingdanList: [], + page: 1, + hasMore: true + }); + await this.loadDingdanList(true); + } catch (error) { + console.error('切换类型失败:', error); + } finally { + this.setData({ isSwitching: false }); + wx.hideLoading(); + } + }, + + async loadDingdanList(isRefresh = false) { + if (this.data.isLoading || !this.data.xuanzhongLeixingId) return; + + const loadPage = isRefresh ? 1 : this.data.page; + if (!isRefresh && !this.data.hasMore) return; + + this.setData({ + isLoading: true, + isLoadingMore: !isRefresh + }); + + try { + const res = await request({ + url: '/dingdan/ddhq', + method: 'POST', + data: { + leixing_id: this.data.xuanzhongLeixingId, + page: loadPage, + page_size: this.data.pageSize + } + }); + + this.setData({ + isLoading: false, + isLoadingMore: false, + scrollViewRefreshing: false + }); + + if (res.data.code === 200 || res.data.code === 0) { + const newList = res.data.data.list || []; + const hasMore = res.data.data.has_more || false; + + const processedList = newList.map(item => this.processDingdanItem(item)); + + const updatedList = isRefresh ? processedList : [...this.data.dingdanList, ...processedList]; + + updatedList.sort((a, b) => (b.creat_time || '').localeCompare(a.creat_time || '')); + + this.setData({ + dingdanList: updatedList, + page: loadPage, + hasMore: hasMore + }); + + if (isRefresh) { + this.setData({ lastRefreshTime: Date.now() }); + } + } else { + wx.showToast({ title: res.data.msg || '加载失败', icon: 'none' }); + } + } catch (error) { + console.error('加载订单失败:', error); + this.setData({ + isLoading: false, + isLoadingMore: false, + scrollViewRefreshing: false + }); + wx.showToast({ title: '网络请求失败', icon: 'none' }); + } + }, + + processDingdanItem(item) { + const ossUrl = app.globalData.ossImageUrl || ''; + let fullTupianUrl = ''; + if (item.tupian) { + fullTupianUrl = !item.tupian.startsWith('http') ? ossUrl + item.tupian : item.tupian; + } else { + const leixing = this.data.shangpinleixing.find(l => l.id === item.leixing_id); + fullTupianUrl = leixing ? leixing.full_tupian_url : '/images/default-order.png'; + } + + const isZhiding = item.zhuangtai === 7; + + let zhidingAvatar = ''; + if (item.zhiding_avatar) { + zhidingAvatar = !item.zhiding_avatar.startsWith('http') ? ossUrl + item.zhiding_avatar : item.zhiding_avatar; + } + const zhidingNicheng = item.zhiding_nicheng || ''; + + return { + ...item, + full_tupian_url: fullTupianUrl, + isZhiding: isZhiding, + isPingtai: item.pingtai == 1, + isShangjia: item.pingtai == 2, + zhiding_avatar_full: zhidingAvatar, + zhiding_nicheng: zhidingNicheng + }; + }, + + goToChongzhiPage(failureType, huiyuanId = null, requiredYajin = 0) { + let url = '/pages/dashouchongzhi/dashouchongzhi'; + let params = {}; + switch (failureType) { + case 'huiyuan': + params = { needScroll: '0', scrollTo: 'member' }; + break; + case 'yajin': + params = { needScroll: '1', scrollTo: 'bottom', requiredYajin: requiredYajin }; + break; + case 'jifen': + params = { needScroll: '1', scrollTo: 'bottom' }; + break; + } + if (huiyuanId) params.huiyuanId = huiyuanId; + const queryString = Object.keys(params).map(key => `${key}=${encodeURIComponent(params[key])}`).join('&'); + const fullUrl = `${url}?${queryString}`; + wx.navigateTo({ + url: fullUrl, + fail: (err) => { + console.error('跳转失败:', err); + wx.showToast({ title: '跳转失败,请稍后重试', icon: 'none' }); + } + }); + }, + + async onQiangdanTap(e) { + const dingdanItem = e.currentTarget.dataset.item; + if (!dingdanItem) return; + + if (!this.data.dashoustatus || this.data.dashoustatus != 1) { + wx.showToast({ title: '请先开启打手身份', icon: 'none' }); + return; + } + if (this.data.zhuanghaoStatus != 1) { + wx.showToast({ title: '账号异常,无法接单', icon: 'none' }); + return; + } + if (this.data.dashouzhuangtai != 1) { + wx.showToast({ title: '您有订单正在接待中', icon: 'none' }); + return; + } + if (dingdanItem.isZhiding) { + if (!this.data.uid || this.data.uid != dingdanItem.zhiding_uid) { + wx.showToast({ title: '此订单为指定单', icon: 'none' }); + return; + } + } + if (dingdanItem.yaoqiuleixing == 1) { + const hasHuiyuan = this.data.huiyuanList.some(h => h.huiyuanid == dingdanItem.huiyuan_id); + if (!hasHuiyuan) { + wx.showToast({ title: '未开通对应会员,无法抢单', icon: 'none', duration: 2000 }); + setTimeout(() => this.goToChongzhiPage('huiyuan', dingdanItem.huiyuan_id), 500); + return; + } + } + if (dingdanItem.yaoqiuleixing == 2) { + if (this.data.yajin < dingdanItem.yajin) { + wx.showToast({ title: `押金不足,需${dingdanItem.yajin}`, icon: 'none', duration: 2000 }); + setTimeout(() => this.goToChongzhiPage('yajin', null, dingdanItem.yajin), 500); + return; + } + } + if (this.data.jifen < 5) { + wx.showToast({ title: '积分不足,至少需要5积分', icon: 'none', duration: 2000 }); + setTimeout(() => this.goToChongzhiPage('jifen'), 500); + return; + } + + const that = this; + wx.showModal({ + title: '确认抢单', + content: `确定要抢此订单吗?${dingdanItem.isZhiding ? '(指定订单)' : ''}`, + success: async function (res) { + if (res.confirm) { + wx.showLoading({ title: '抢单中...', mask: true }); + try { + const qiangdanRes = await request({ + url: '/dingdan/qiangdan', + method: 'POST', + data: { dingdan_id: dingdanItem.dingdan_id } + }); + wx.hideLoading(); + if (qiangdanRes.data.code === 200 || qiangdanRes.data.code === 0) { + that.showQiangdanSuccessEffect(); + if (app.globalData) { + app.globalData.dashouzhuangtai = 0; + that.setData({ dashouzhuangtai: 0 }); + } + const newList = that.data.dingdanList.filter(item => item.dingdan_id !== dingdanItem.dingdan_id); + that.setData({ dingdanList: newList }); + } else { + wx.showToast({ title: qiangdanRes.data.msg || '抢单失败', icon: 'none' }); + } + } catch (error) { + wx.hideLoading(); + console.error('抢单请求失败:', error); + wx.showToast({ title: '网络错误,抢单失败', icon: 'none' }); + } + } + } + }); + }, + + showQiangdanSuccessEffect() { + wx.showToast({ title: '抢单成功!', icon: 'success', duration: 2000 }); + }, + + onViewDetail(e) { + const { type, content } = e.currentTarget.dataset; + wx.showModal({ + title: type === 'jieshao' ? '订单介绍' : '订单备注', + content: content, + showCancel: false, + confirmText: '知道了' + }); + }, + + onReachBottom() { + const now = Date.now(); + if (now - this.data.lastRefreshTime < this.data.refreshCooldown) { + wx.showToast({ + title: `操作太快,请${Math.ceil((this.data.refreshCooldown - (now - this.data.lastRefreshTime)) / 1000)}秒后再试`, + icon: 'none', + duration: 1500 + }); + return; + } + if (this.data.isLoading || this.data.isLoadingMore) return; + if (this.data.hasMore && !this.data.isLoading && this.data.xuanzhongLeixingId) { + this.setData({ page: this.data.page + 1 }); + this.loadDingdanList(false); + } + }, + + onPullDownRefresh() { + const now = Date.now(); + if (now - this.data.lastRefreshTime < this.data.refreshCooldown) { + this.setData({ scrollViewRefreshing: false }); + wx.showToast({ + title: `操作太快,请${Math.ceil((this.data.refreshCooldown - (now - this.data.lastRefreshTime)) / 1000)}秒后再试`, + icon: 'none', + duration: 1500 + }); + return; + } + if (this.data.isLoading) { + this.setData({ scrollViewRefreshing: false }); + return; + } + this.setData({ + scrollViewRefreshing: true, + lastRefreshTime: now + }); + if (this.data.xuanzhongLeixingId) { + this.setData({ page: 1, hasMore: true }); + this.loadDingdanList(true); + } else { + this.setData({ scrollViewRefreshing: false }); + } + }, + + goToDashouRegister() { + wx.navigateTo({ + url: '/pages/dashouduan/dashouduan' + }); + } +}); \ No newline at end of file diff --git a/pages/jiedanchi/jiedanchi.json b/pages/jiedanchi/jiedanchi.json new file mode 100644 index 0000000..ea51a5d --- /dev/null +++ b/pages/jiedanchi/jiedanchi.json @@ -0,0 +1,9 @@ +{ + "navigationBarTitleText": "接单池", + "enablePullDownRefresh": false, + "backgroundTextStyle": "dark", + "backgroundColor": "#f5f7fa", + "usingComponents": { + "global-notification": "/components/global-notification/global-notification" + } + } \ No newline at end of file diff --git a/pages/jiedanchi/jiedanchi.wxml b/pages/jiedanchi/jiedanchi.wxml new file mode 100644 index 0000000..027df01 --- /dev/null +++ b/pages/jiedanchi/jiedanchi.wxml @@ -0,0 +1,263 @@ + + + + + + + + 🔒 + 功能未开启 + {{unauthorizedMsg}} + 请先注册为服务者,即可使用抢单功能 + + 立即注册 + + + + + + + + + + + + + + + + + + {{item.jieshao}} + + + + + + + + + + + + + + + 正在刷新... + 下拉刷新订单 + + + + + + + + 请先选择上方订单类型 + + + + + 当前类型暂无待抢订单 + + + + + + + + + {{item.creat_time}} + + 平台订单 + + + + + 指定 + + {{item.zhiding_nicheng || '指定打手'}} + UID: {{item.zhiding_uid || '未知'}} + + + + + + + 介绍: + {{item.jieshao || '暂无介绍'}} + [点击查看] + + + + + + 备注: + {{item.beizhu}} + [点击查看] + + + + + + 💰 + 分佣 + {{item.dashou_fencheng || 0}} + + + + 立即抢单 + + + + + + + + + + {{item.creat_time}} + + 商家发单 + + + + + 指定 + + {{item.zhiding_nicheng || '指定打手'}} + UID: {{item.zhiding_uid || '未知'}} + + + + + + {{item.sjnicheng || '未知商家'}} + + + + + 订单介绍: + {{item.jieshao || '暂无介绍'}} + [点击查看] + + + + + 商家备注: + {{item.beizhu}} + [点击查看] + + + + + + 💰 + 分佣 + {{item.dashou_fencheng || 0}} + + + + 立即抢单 + + + + + + + + + 加载更多订单中... + + + 没有更多订单了 + + + + + + + + 订单加载中... + + + + + + + \ No newline at end of file diff --git a/pages/jiedanchi/jiedanchi.wxss b/pages/jiedanchi/jiedanchi.wxss new file mode 100644 index 0000000..5784b03 --- /dev/null +++ b/pages/jiedanchi/jiedanchi.wxss @@ -0,0 +1,655 @@ +/* pages/qiangdan/qiangdan.wxss */ +.qiangdan-page { + height: 100vh; + display: flex; + flex-direction: column; + background-color: #f5f7fa; + overflow: hidden; + position: relative; + } + + /* ====== 授权提示页(机甲风格)====== */ + .unauthorized-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: radial-gradient(ellipse at top, #0b0f1a, #03050a); + z-index: 100; + } + + .unauthorized-card { + width: 600rpx; + padding: 60rpx 40rpx; + background: linear-gradient(145deg, #1a1f30, #0f1422); + border-radius: 60rpx; + position: relative; + overflow: hidden; + border: 2rpx solid rgba(0, 247, 255, 0.3); + box-shadow: 0 30rpx 60rpx rgba(0, 0, 0, 0.8), 0 0 30rpx rgba(0, 247, 255, 0.2); + } + + .card-glow { + position: absolute; + top: -20%; + left: -20%; + width: 140%; + height: 140%; + background: radial-gradient(circle at 30% 30%, rgba(0, 247, 255, 0.2), transparent 60%); + animation: rotateGlow 10s linear infinite; + z-index: 0; + } + + @keyframes rotateGlow { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + .card-content-unauth { + position: relative; + z-index: 2; + display: flex; + flex-direction: column; + align-items: center; + color: #ffffff; + } + + .icon-lock { + font-size: 100rpx; + margin-bottom: 30rpx; + background: rgba(255, 255, 255, 0.1); + width: 160rpx; + height: 160rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + border: 4rpx solid #00f7ff; + box-shadow: 0 0 50rpx #00f7ff; + text-shadow: 0 0 20rpx #00f7ff; + } + + .title { + font-size: 48rpx; + font-weight: 700; + letter-spacing: 4rpx; + margin-bottom: 20rpx; + text-shadow: 0 0 15rpx #00f7ff; + background: linear-gradient(135deg, #ffffff, #aaddff); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + + .message { + font-size: 32rpx; + color: #ffaa00; + margin-bottom: 16rpx; + font-weight: 600; + text-shadow: 0 0 10rpx #ffaa00; + } + + .description { + font-size: 28rpx; + color: #a0b0d0; + margin-bottom: 50rpx; + text-align: center; + } + + .btn-register { + position: relative; + padding: 24rpx 80rpx; + background: linear-gradient(145deg, #2a3050, #181e38); + border-radius: 60rpx; + overflow: hidden; + border: 2rpx solid #00f7ff; + box-shadow: 0 10rpx 30rpx rgba(0, 247, 255, 0.4); + transition: transform 0.2s; + } + + .btn-register:active { + transform: scale(0.96); + } + + .btn-text { + font-size: 32rpx; + color: #fff; + font-weight: 700; + letter-spacing: 4rpx; + position: relative; + z-index: 2; + text-shadow: 0 0 10rpx #00f7ff; + } + + .btn-shine { + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient( + to right, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.2) 50%, + rgba(255, 255, 255, 0) 100% + ); + transform: rotate(30deg); + animation: btn-shimmer 2.8s infinite linear; + z-index: 1; + } + + @keyframes btn-shimmer { + 0% { transform: translateX(-100%) rotate(30deg); } + 100% { transform: translateX(100%) rotate(30deg); } + } + + /* ====== 原有样式(保持不变,仅略作调整以兼容提示页)====== */ + .leixing-quyu { + padding: 30rpx 0 20rpx; + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + border-bottom: 1rpx solid #eaeaea; + position: relative; + z-index: 10; + flex-shrink: 0; + } + + .leixing-scroll { + width: 100%; + white-space: nowrap; + height: 180rpx; + } + + .leixing-container { + display: inline-flex; + padding: 0 30rpx; + } + + .leixing-item { + display: inline-flex; + flex-direction: column; + align-items: center; + width: 160rpx; + margin-right: 24rpx; + position: relative; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + } + + .leixing-active { + transform: translateY(-6rpx); + } + + .guangyun-effect { + position: absolute; + top: -12rpx; + left: -12rpx; + right: -12rpx; + bottom: -12rpx; + background: radial-gradient(circle at center, rgba(82, 196, 26, 0.15) 0%, transparent 70%); + border-radius: 40rpx; + z-index: 0; + animation: guangyun-pulse 2s infinite ease-in-out; + } + + @keyframes guangyun-pulse { + 0%, 100% { opacity: 0.6; transform: scale(1); } + 50% { opacity: 0.9; transform: scale(1.05); } + } + + .leixing-tupian { + width: 120rpx; + height: 120rpx; + border-radius: 28rpx; + z-index: 1; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; + } + + .leixing-active .leixing-tupian { + box-shadow: 0 16rpx 40rpx rgba(82, 196, 26, 0.3); + border: 4rpx solid #52c41a; + } + + .leixing-jieshao { + margin-top: 16rpx; + font-size: 24rpx; + color: #666; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: center; + z-index: 1; + } + + .leixing-active .leixing-jieshao { + color: #52c41a; + font-weight: 600; + } + + .fenge-xian { + height: 16rpx; + background: linear-gradient(to bottom, #f0f2f5, #f8f9fa); + flex-shrink: 0; + } + + .dingdan-container { + flex: 1; + overflow: hidden; + } + + .dingdan-scroll-view { + height: 100%; + -webkit-overflow-scrolling: touch; + } + + .refresher-container { + height: 100rpx; + display: flex; + align-items: center; + justify-content: center; + } + + .refreshing-text, + .pull-down-text { + font-size: 26rpx; + color: #999; + } + + .empty-tip { + padding: 120rpx 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + .tip-icon { + width: 160rpx; + height: 160rpx; + opacity: 0.4; + margin-bottom: 30rpx; + } + + .tip-text { + font-size: 28rpx; + color: #aaa; + } + + .dingdan-card { + margin: 20rpx 24rpx; + border-radius: 20rpx; + padding: 28rpx; + position: relative; + overflow: hidden; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.06); + transition: all 0.2s ease; + border: 1rpx solid rgba(0, 0, 0, 0.08); + } + + .dingdan-card:active { + transform: scale(0.995); + } + + .pingtai-card { + background: linear-gradient(145deg, #f8fff8 0%, #f0f9f0 100%); + } + + .shangjia-card { + background: linear-gradient(145deg, #f9f0ff 0%, #f3e8ff 100%); + } + + .card-top { + display: flex; + align-items: center; + margin-bottom: 20rpx; + padding-bottom: 16rpx; + border-bottom: 1rpx dashed #e0e0e0; + } + + .creat-time { + font-size: 26rpx; + color: #555; + font-weight: 500; + } + + .top-line { + flex: 1; + height: 1rpx; + background: linear-gradient(to right, transparent, #ddd, transparent); + margin: 0 20rpx; + } + + .pingtai-tag { + font-size: 24rpx; + color: #666; + padding: 6rpx 16rpx; + background: rgba(0, 0, 0, 0.04); + border-radius: 20rpx; + } + + .zhiding-row { + display: flex; + align-items: center; + margin-bottom: 20rpx; + padding: 10rpx 16rpx; + background: rgba(255, 77, 79, 0.08); + border-radius: 12rpx; + border-left: 4rpx solid #ff4d4f; + gap: 10rpx; + } + + .zhiding-icon { + font-size: 26rpx; + margin-right: 4rpx; + } + + .zhiding-avatar { + width: 48rpx; + height: 48rpx; + border-radius: 50%; + border: 2rpx solid #ff4d4f; + object-fit: cover; + } + + .zhiding-nicheng { + font-size: 24rpx; + color: #ff4d4f; + font-weight: 500; + max-width: 120rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .zhiding-uid { + font-size: 24rpx; + color: #ff4d4f; + font-weight: 400; + flex: 1; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .card-content { + display: flex; + margin-bottom: 20rpx; + } + + .neirong-tupian { + width: 140rpx; + height: 140rpx; + border-radius: 50%; + margin-right: 24rpx; + flex-shrink: 0; + box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08); + object-fit: cover; + } + + .jieshao-box { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 140rpx; + padding: 16rpx 0; + } + + .jieshao-label { + font-size: 26rpx; + color: #333; + font-weight: 600; + margin-bottom: 8rpx; + } + + .jieshao-text { + font-size: 26rpx; + color: #666; + line-height: 1.4; + } + + .jieshao-ellipsis .jieshao-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + text-overflow: ellipsis; + } + + .shangjia-info { + display: flex; + align-items: center; + margin-bottom: 20rpx; + } + + .shangjia-touxiang { + width: 90rpx; + height: 90rpx; + border-radius: 50%; + margin-right: 20rpx; + border: 2rpx solid rgba(114, 46, 209, 0.15); + box-shadow: 0 6rpx 18rpx rgba(114, 46, 209, 0.12); + object-fit: cover; + } + + .shangjia-nicheng { + font-size: 28rpx; + color: #333; + font-weight: 600; + } + + .shangjia-jieshao, + .shangjia-beizhu { + margin-bottom: 16rpx; + padding: 14rpx 18rpx; + background: rgba(255, 255, 255, 0.6); + border-radius: 12rpx; + } + + .shangjia-jieshao .jieshao-label, + .shangjia-beizhu .beizhu-label { + display: block; + margin-bottom: 6rpx; + font-size: 24rpx; + } + + .shangjia-jieshao .jieshao-text, + .shangjia-beizhu .beizhu-text { + font-size: 24rpx; + color: #555; + line-height: 1.4; + } + + .shangjia-jieshao.jieshao-ellipsis .jieshao-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + } + + .shangjia-beizhu.beizhu-ellipsis .beizhu-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + } + + .beizhu-box { + margin-bottom: 20rpx; + padding: 14rpx 18rpx; + background: rgba(255, 255, 255, 0.6); + border-radius: 12rpx; + } + + .beizhu-label { + font-size: 24rpx; + color: #333; + font-weight: 600; + margin-bottom: 6rpx; + display: block; + } + + .beizhu-text { + font-size: 24rpx; + color: #666; + line-height: 1.4; + } + + .beizhu-ellipsis .beizhu-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + } + + .view-more { + display: block; + font-size: 22rpx; + color: #1890ff; + margin-top: 6rpx; + text-align: right; + } + + .card-bottom { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 20rpx; + border-top: 1rpx solid rgba(0, 0, 0, 0.06); + } + + .fenyong-box { + display: flex; + align-items: baseline; + } + + .fenyong-icon { + font-size: 30rpx; + margin-right: 10rpx; + } + + .fenyong-text { + font-size: 24rpx; + color: #999; + margin-right: 6rpx; + } + + .fenyong-price { + font-size: 40rpx; + color: #ff4d4f; + font-weight: 700; + margin-right: 4rpx; + } + + .fenyong-unit { + font-size: 24rpx; + color: #999; + } + + .qiangdan-btn { + position: relative; + padding: 18rpx 40rpx; + border-radius: 50rpx; + overflow: hidden; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12); + } + + .pingtai-btn { + background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); + } + + .shangjia-btn { + background: linear-gradient(135deg, #722ed1 0%, #9254de 100%); + } + + .btn-text { + font-size: 28rpx; + color: white; + font-weight: 600; + letter-spacing: 1rpx; + position: relative; + z-index: 2; + } + + .btn-shine { + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient( + to right, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.3) 50%, + rgba(255, 255, 255, 0) 100% + ); + transform: rotate(30deg); + animation: btn-shimmer 3s infinite linear; + z-index: 1; + } + + @keyframes btn-shimmer { + 0% { transform: translateX(-100%) rotate(30deg); } + 100% { transform: translateX(100%) rotate(30deg); } + } + + .qiangdan-btn:active { + transform: scale(0.96); + } + + .loading-more, + .no-more { + padding: 30rpx 0; + text-align: center; + } + + .loading-text { + font-size: 26rpx; + color: #999; + } + + .no-more-text { + font-size: 26rpx; + color: #ccc; + } + + .loading-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.9); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 9999; + } + + .loading-spinner { + width: 70rpx; + height: 70rpx; + border: 6rpx solid #f0f0f0; + border-top: 6rpx solid #52c41a; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 24rpx; + } + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + .loading-mask-text { + font-size: 28rpx; + color: #666; + } \ No newline at end of file diff --git a/pages/jiedanchi2/jiedanchi2.js b/pages/jiedanchi2/jiedanchi2.js new file mode 100644 index 0000000..0e33db9 --- /dev/null +++ b/pages/jiedanchi2/jiedanchi2.js @@ -0,0 +1,66 @@ +// pages/jiedanchi2/jiedanchi2.js +Page({ + + /** + * 页面的初始数据 + */ + data: { + + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + + }, + + /** + * 生命周期函数--监听页面初次渲染完成 + */ + onReady() { + + }, + + /** + * 生命周期函数--监听页面显示 + */ + onShow() { + + }, + + /** + * 生命周期函数--监听页面隐藏 + */ + onHide() { + + }, + + /** + * 生命周期函数--监听页面卸载 + */ + onUnload() { + + }, + + /** + * 页面相关事件处理函数--监听用户下拉动作 + */ + onPullDownRefresh() { + + }, + + /** + * 页面上拉触底事件的处理函数 + */ + onReachBottom() { + + }, + + /** + * 用户点击右上角分享 + */ + onShareAppMessage() { + + } +}) \ No newline at end of file diff --git a/pages/jiedanchi2/jiedanchi2.json b/pages/jiedanchi2/jiedanchi2.json new file mode 100644 index 0000000..3928faa --- /dev/null +++ b/pages/jiedanchi2/jiedanchi2.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/pages/jiedanchi2/jiedanchi2.wxml b/pages/jiedanchi2/jiedanchi2.wxml new file mode 100644 index 0000000..fdd9cd8 --- /dev/null +++ b/pages/jiedanchi2/jiedanchi2.wxml @@ -0,0 +1,2 @@ + +pages/jiedanchi2/jiedanchi2.wxml \ No newline at end of file diff --git a/pages/jiedanchi2/jiedanchi2.wxss b/pages/jiedanchi2/jiedanchi2.wxss new file mode 100644 index 0000000..fae36a0 --- /dev/null +++ b/pages/jiedanchi2/jiedanchi2.wxss @@ -0,0 +1 @@ +/* pages/jiedanchi2/jiedanchi2.wxss */ \ No newline at end of file diff --git a/pages/jinpaids/jinpaids.js b/pages/jinpaids/jinpaids.js new file mode 100644 index 0000000..aef8cef --- /dev/null +++ b/pages/jinpaids/jinpaids.js @@ -0,0 +1,346 @@ +// pages/jinpaids/jinpaids.js +import request from '../../utils/request.js' +import popupService from '../../services/popupService.js' + +const app = getApp() + +Page({ + data: { + // OSS地址 + ossImageUrl: '', + + // 图标路径 + icons: { + jinpai: '', + refresh: '', + copy: '', + qiangdan: '', + chongzhi: '', + dingdan: '', + chufa: '', + paihang: '', + xiugai: '', + tixian: '', + rightArrow: '' + }, + + // 用户基本信息 + uid: '', + touxiang: '', + avatarUrl: '', + dashouNicheng: '', + + // 打手核心数据 + chenghao: '', + yongjin: '0.00', + zonge: '0.00', + yajin: '0.00', + jinfen: '0', + chengjiaoliang: '0', + zaixianZhuangtai: 0, + dashouzhuangtai: '', + clumber: [], + + // 金牌专属数据 + jiedanZongliang: '0', + jinriJiedan: '0.00', + zuijinTixian: '0.00', + + // 页面状态 + isLoading: false, + lastRefreshTime: 0, + canRefresh: true, + + // 计算数据 + chengjiaoRate: '0%', + chengjiaoAngle: 0, + tixianProgress: 0, + + // 邀请人缓存(管事) + inviterCache: null, + chenghaoList: [] // 新增 + }, + + onLoad() { + const ossUrl = app.globalData.ossImageUrl || '' + const iconBase = 'beijing/tubiao/' + + this.setData({ + ossImageUrl: ossUrl, + 'icons.jinpai': ossUrl + iconBase + 'jinpai.png', + 'icons.refresh': ossUrl + iconBase + 'shuaxin.png', + 'icons.copy': ossUrl + iconBase + 'fuzhi.png', + 'icons.qiangdan': ossUrl + iconBase + 'qiangdan.png', + 'icons.chongzhi': ossUrl + iconBase + 'chongzhi.png', + 'icons.dingdan': ossUrl + iconBase + 'dingdan.png', + 'icons.chufa': ossUrl + iconBase + 'chufa.png', + 'icons.paihang': ossUrl + iconBase + 'paihang.png', + 'icons.xiugai': ossUrl + iconBase + 'xiugai.png', + 'icons.tixian': ossUrl + iconBase + 'tixian.png', + 'icons.rightArrow': ossUrl + iconBase + 'jiantou.png', + 'icons.guize': ossUrl + iconBase + 'guize.png', + 'icons.kaohe': ossUrl + iconBase + 'kaohe.png', + }) + + this.loadUserInfo() + this.getJinpaiInfo() + + if (!app.globalData.hasShownPopupOnColdStart) { + app.globalData.hasShownPopupOnColdStart = true; + setTimeout(() => { + popupService.checkAndShow(this, 'jinpaids'); + }, 300); + } + }, + + onHide() { + const popupComp = this.selectComponent('#popupNotice'); + if (popupComp && popupComp.cleanup) { + popupComp.cleanup(); + } + }, + + onShow() { + this.registerNotificationComponent() + this.refreshJinpaiInfo() + this.fetchChenghaoList() // 新增 + const inviterCache = wx.getStorageSync('inviterCache') || null + this.setData({ inviterCache }) + }, + + registerNotificationComponent() { + const notificationComp = this.selectComponent('#global-notification') + if (notificationComp && notificationComp.showNotification) { + app.globalData.globalNotification = { + show: (data) => notificationComp.showNotification(data), + hide: () => notificationComp.hideNotification() + } + } + }, + + loadUserInfo() { + const uid = wx.getStorageSync('uid') || '' + const touxiang = wx.getStorageSync('touxiang') || '' + const ossUrl = app.globalData.ossImageUrl || '' + const defaultAvatar = ossUrl + (app.globalData.morentouxiang || 'avatar/default.jpg') + + let avatarUrl = defaultAvatar + if (touxiang) { + avatarUrl = ossUrl + (touxiang.startsWith('/') ? touxiang : '/' + touxiang) + } + + const dashouNicheng = app.globalData.dashouNicheng || wx.getStorageSync('nicheng') || '金牌选手' + + this.setData({ uid, touxiang, avatarUrl, dashouNicheng }) + }, + + async getJinpaiInfo() { + this.setData({ isLoading: true }) + + try { + const res = await request({ + url: '/yonghu/dashouxinxi', + method: 'POST' + }) + + if (res && res.data.code === 200) { + const data = res.data.data + + const updateData = { + dashouNicheng: data.dashounicheng || '', + zhanghaoStatus: data.zhanghaostatus || '', + yongjin: data.yongjin || '0.00', + zonge: data.zonge || '0.00', + yajin: data.yajin || '0.00', + chenghao: data.chenghao || '', + jinfen: data.jifen || '0', + chengjiaoliang: data.chengjiaoliang || '0', + zaixianZhuangtai: data.zaixianzhuangtai || 0, + dashouzhuangtai: data.dashouzhuangtai || '', + clumber: data.clumber || [], + jiedanZongliang: data.jiedanZongliang || '0', + jinriJiedan: data.jinriJiedan || '0.00', + zuijinTixian: data.zuijinTixian || '0.00', + } + + const jiedanTotal = parseFloat(updateData.jiedanZongliang) || 0 + const chengjiaoTotal = parseFloat(updateData.chengjiaoliang) || 0 + const rateNumber = jiedanTotal > 0 ? (chengjiaoTotal / jiedanTotal) * 100 : 0 + const chengjiaoRate = rateNumber.toFixed(1) + '%' + const chengjiaoAngle = rateNumber * 3.6 + + const zuijin = parseFloat(updateData.zuijinTixian) || 0 + const zong = parseFloat(updateData.jinriJiedan) || 0 + const tixianProgress = zong > 0 ? Math.min((zuijin / zong) * 100, 100) : 0 + + const globalUpdate = { + dashouNicheng: updateData.dashouNicheng, + zhanghaoStatus: updateData.zhanghaoStatus, + yongjin: updateData.yongjin, + zonge: updateData.zonge, + yajin: updateData.yajin, + chenghao: updateData.chenghao, + jinfen: updateData.jinfen, + chengjiaoliang: updateData.chengjiaoliang, + zaixianZhuangtai: updateData.zaixianZhuangtai, + dashouzhuangtai: updateData.dashouzhuangtai, + clumber: updateData.clumber, + } + Object.assign(app.globalData, globalUpdate) + + wx.setStorageSync('nicheng', updateData.dashouNicheng) + if (data.touxiang) { + wx.setStorageSync('touxiang', data.touxiang) + } + + this.setData({ + ...updateData, + chengjiaoRate, + chengjiaoAngle, + tixianProgress: Math.round(tixianProgress), + isLoading: false + }) + + this.checkJinpaiAndRedirect(updateData.chenghao) + + } else { + wx.showToast({ title: res?.data?.msg || '获取信息失败', icon: 'none' }) + this.setData({ isLoading: false }) + } + } catch (error) { + console.error('获取金牌信息失败:', error) + wx.showToast({ title: '网络错误', icon: 'none' }) + this.setData({ isLoading: false }) + } + }, + + refreshJinpaiInfo() { + const now = Date.now() + const lastTime = this.data.lastRefreshTime + const canRefresh = this.data.canRefresh + if (!canRefresh || (now - lastTime < 3000)) return + + + this.setData({ lastRefreshTime: now, canRefresh: false }) + this.getJinpaiInfo() + this.fetchChenghaoList() // 新增 + setTimeout(() => this.setData({ canRefresh: true }), 3000) + }, + + checkJinpaiAndRedirect(chenghao) { + if (chenghao !== '金牌选手') { + wx.setStorageSync('isJinpai', 0) + wx.redirectTo({ url: '/pages/dashouduan/dashouduan' }) + } else { + wx.setStorageSync('isJinpai', 1) + } + }, + + // 联系管事(与打手端完全一致,增加容错) + async contactInviter() { + wx.showLoading({ title: '获取管事信息...', mask: true }) + try { + const res = await request({ url: '/yonghu/hqwdyqr', method: 'POST' }) + if (res && res.data.code === 200) { + const inviter = res.data.data + const ossImageUrl = getApp().globalData.ossImageUrl || '' + const fullAvatar = inviter.touxiang + ? (inviter.touxiang.startsWith('http') ? inviter.touxiang : ossImageUrl + inviter.touxiang) + : '' + + // 缓存邀请人信息 + const cacheData = { + uid: inviter.uid, + nicheng: inviter.nicheng || '', + avatar: fullAvatar + } + wx.setStorageSync('inviterCache', cacheData) + this.setData({ inviterCache: cacheData }) + + // 确保打手身份并连接 + const app = getApp() + const myUid = wx.getStorageSync('uid') + + // 如果当前不是打手身份,则切换并重连 + if (app.globalData.currentRole !== 'dashou') { + app.globalData.currentRole = 'dashou' + wx.setStorageSync('currentRole', 'dashou') + if (app.switchRoleAndReconnect) { + await app.switchRoleAndReconnect('dashou') + } else if (app.connectWithIdentity) { + // 降级方案:直接用打手身份连接 + await app.connectWithIdentity('dashou', 'Ds' + myUid, false) + } + } else { + // 已为打手,确保连接存活 + if (app.connectForCurrentRole) { + app.connectForCurrentRole() + } else if (app.ensureConnection) { + app.ensureConnection() + } + } + + // 构造私聊参数 + const param = { + toUserId: 'Gs' + inviter.uid, + toName: inviter.nicheng || '管事', + toAvatar: fullAvatar + } + wx.navigateTo({ + url: '/pages/liaotian/liaotian?data=' + encodeURIComponent(JSON.stringify(param)), + fail: (err) => { + console.error('跳转私聊失败', err) + wx.showToast({ title: '打开聊天失败', icon: 'none' }) + } + }) + } else { + wx.showToast({ title: res?.data?.msg || '获取失败', icon: 'none' }) + } + } catch (e) { + console.error('联系管事出错:', e) + wx.showToast({ title: '网络错误', icon: 'none' }) + } finally { + wx.hideLoading() + } + }, + + copyUid() { + const { uid } = this.data + if (!uid) return wx.showToast({ title: 'UID不存在', icon: 'none' }) + wx.setClipboardData({ + data: uid, + success: () => wx.showToast({ title: '已复制UID', icon: 'success' }) + }) + }, + + previewAvatar() { + if (this.data.avatarUrl) wx.previewImage({ urls: [this.data.avatarUrl] }) + }, + + goToWithdraw() { wx.navigateTo({ url: '/pages/tixian/tixian' }) }, + goToReceiveOrder() { wx.navigateTo({ url: '/pages/jiedan/jiedan' }) }, + goToRecharge() { wx.navigateTo({ url: '/pages/dashouchongzhi/dashouchongzhi' }) }, + goToMyOrders() { wx.navigateTo({ url: '/pages/dashoudingdan/dashoudingdan' }) }, + goToMyPunishment() { wx.navigateTo({ url: '/pages/cfss/cfss/cfss' }) }, + goToRanking() { wx.navigateTo({ url: '/pages/dashoupaihang/dashoupaihang' }) }, + goToModifyInfo() { wx.navigateTo({ url: '/pages/dashouxiugai/dashouxiugai' }) }, + goToRules() { wx.navigateTo({ url: '/pages/dashouguize/dashouguize' }) }, + // 跳转到考核金牌 +goToKaohe() { + wx.navigateTo({ url: '/pages/kaohe_jinpai/kaohe_jinpai' }); + }, + + async fetchChenghaoList() { + try { + const res = await request({ + url: '/dengji/dsbqhq', + method: 'POST' + }); + if (res && res.data.code === 0 && res.data.data) { + this.setData({ chenghaoList: res.data.data.chenghao_list || [] }); + } + } catch (e) { + console.error('获取称号标签失败', e); + } +}, +}) \ No newline at end of file diff --git a/pages/jinpaids/jinpaids.json b/pages/jinpaids/jinpaids.json new file mode 100644 index 0000000..f3d656c --- /dev/null +++ b/pages/jinpaids/jinpaids.json @@ -0,0 +1,9 @@ +{ + "usingComponents": { + "global-notification": "/components/global-notification/global-notification", + "chenghao-tag": "/components/chenghao-tag/chenghao-tag" + }, + "navigationBarTitleText": "金牌用户", + "navigationBarBackgroundColor": "#0B2A4A", + "navigationBarTextStyle": "white" + } \ No newline at end of file diff --git a/pages/jinpaids/jinpaids.wxml b/pages/jinpaids/jinpaids.wxml new file mode 100644 index 0000000..5912b19 --- /dev/null +++ b/pages/jinpaids/jinpaids.wxml @@ -0,0 +1,193 @@ + + + + + + + + + + 金牌选手 + + + + 刷新 + + + + + + + + + + + + + + + + + + + + + + + + + + + + 可提现金额 + + {{yongjin}} + + + + + + 立即提现 + + + + + + + + 总赚取 + {{zonge}}元 + + + 保证金 + {{yajin}}元 + + + 积分 + {{jinfen}} + + + 接单总量 + {{jiedanZongliang}} + + + 成交总量 + {{chengjiaoliang}} + + + 今日接单 + {{jinriJiedan}}元 + + + + + + + 成交率 + + + {{chengjiaoRate}} + + + + 最近提现占比 + + + {{tixianProgress}}% + + + 最近提现:{{zuijinTixian}}元 + 今日赚取:{{jinriJiedan}}元 + + + + + + + + + + {{inviterCache.nicheng || '我的管事'}} + ID: {{inviterCache.uid}} + + + 联系我的邀请人 + + + + + + 我的管事 + 点击联系 + + + 联系我的邀请人 + + + + + + + + 抢单大厅 + + + + 充值会员保证金 + + + + 我的订单 + + + + 我的处罚 + + + + 排行榜 + + + + 修改资料 + + + + 用户规则 + + + + 考核金牌 + + + + + + + + + + + + \ No newline at end of file diff --git a/pages/jinpaids/jinpaids.wxss b/pages/jinpaids/jinpaids.wxss new file mode 100644 index 0000000..162eae6 --- /dev/null +++ b/pages/jinpaids/jinpaids.wxss @@ -0,0 +1,610 @@ +/* pages/jinpaids/jinpaids.wxss - 金牌打手机甲风格(CSS变量版) */ + +/* ========== 全局 ========== */ +page { + background: #0B1A2F; + min-height: 100vh; + font-family: 'Avenir', 'PingFang SC', 'Helvetica Neue', sans-serif; + color: #fff; + } + + .bg-grid { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + linear-gradient(rgba(0, 160, 255, 0.1) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 160, 255, 0.1) 1px, transparent 1px); + background-size: 50rpx 50rpx; + pointer-events: none; + z-index: 0; + } + + .bg-energy { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at 20% 30%, rgba(0,200,255,0.1) 0%, transparent 40%), + radial-gradient(circle at 80% 70%, rgba(0,100,255,0.1) 0%, transparent 40%); + animation: energyFlow 20s linear infinite; + pointer-events: none; + z-index: 0; + } + + @keyframes energyFlow { + 0% { opacity: 0.3; } + 50% { opacity: 0.6; } + 100% { opacity: 0.3; } + } + + .page-container { + position: relative; + z-index: 2; + min-height: 100vh; + padding: 30rpx 25rpx; + box-sizing: border-box; + } + + /* ========== 顶部栏 ========== */ + .top-bar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30rpx; + margin-top: 20rpx; + } + .jinpai-badge { + display: flex; + align-items: center; + } + .jinpai-icon { + width: 60rpx; + height: 60rpx; + margin-right: 15rpx; + } + /* 火苗图标燃烧动画 */ + .fire-icon { + animation: fireBurn 0.8s infinite alternate ease-in-out; + filter: drop-shadow(0 0 10rpx #ffaa00); + } + @keyframes fireBurn { + 0% { transform: scale(1) rotate(-3deg); filter: drop-shadow(0 0 10rpx #ffaa00); } + 25% { transform: scale(1.1) rotate(2deg); filter: drop-shadow(0 0 20rpx #ff5500); } + 50% { transform: scale(1.05) rotate(-2deg); filter: drop-shadow(0 0 30rpx #ffaa00); } + 75% { transform: scale(1.15) rotate(3deg); filter: drop-shadow(0 0 40rpx #ffaa00); } + 100% { transform: scale(1) rotate(-3deg); filter: drop-shadow(0 0 10rpx #ffaa00); } + } + .jinpai-title { + font-size: 40rpx; + font-weight: bold; + color: #ffaa00; + text-shadow: 0 0 30rpx #ffaa00, 0 0 60rpx #ffaa00; + letter-spacing: 2rpx; + } + .refresh-btn { + display: flex; + align-items: center; + background: rgba(0,40,70,0.8); + border: 2rpx solid #00a6ff; + clip-path: polygon(0 0, 100% 0, 90% 100%, 0 100%); + padding: 8rpx 25rpx; + } + .refresh-icon { + width: 40rpx; + height: 40rpx; + margin-right: 10rpx; + } + .refresh-text { + font-size: 26rpx; + color: #00a6ff; + } + .refresh-btn:active { + opacity: 0.8; + } + + /* ========== 用户信息 ========== */ + .user-section { + display: flex; + background: rgba(0,30,50,0.5); + backdrop-filter: blur(20px); + border: 2rpx solid #00a6ff; + clip-path: polygon(0 0, 100% 0, 95% 100%, 0 100%); + padding: 30rpx; + margin-bottom: 40rpx; + } + .avatar-wrap { + position: relative; + width: 120rpx; + height: 120rpx; + margin-right: 30rpx; + } + .avatar { + width: 100%; + height: 100%; + border-radius: 50%; + border: 4rpx solid #00a6ff; + box-shadow: 0 0 50rpx #00a6ff; + object-fit: cover; + } + .avatar-ring { + position: absolute; + top: -10rpx; + left: -10rpx; + right: -10rpx; + bottom: -10rpx; + border-radius: 50%; + border: 2rpx solid #00a6ff; + border-top-color: transparent; + border-bottom-color: transparent; + animation: ringRotate 6s linear infinite; + } + @keyframes ringRotate { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + .user-info { + flex: 1; + } + .name-row { + display: flex; + align-items: center; + margin-bottom: 10rpx; + } + .nickname { + font-size: 40rpx; + font-weight: bold; + color: #fff; + margin-right: 15rpx; + max-width: 250rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .status-tag { + font-size: 22rpx; + padding: 4rpx 16rpx; + border-radius: 30rpx; + background: rgba(255,255,255,0.1); + border: 1rpx solid currentColor; + } + .status-tag.online { color: #00ff80; border-color: #00ff80; } + .status-tag.offline { color: #aaa; border-color: #aaa; } + .uid-row { + display: flex; + align-items: center; + background: rgba(0,50,80,0.6); + border-left: 4rpx solid #00a6ff; + padding: 8rpx 20rpx; + margin-bottom: 10rpx; + width: fit-content; + } + .uid-label { + font-size: 24rpx; + color: #9cf; + margin-right: 10rpx; + } + .uid-value { + font-size: 24rpx; + color: #fff; + font-family: monospace; + margin-right: 15rpx; + } + .copy-icon { + width: 30rpx; + height: 30rpx; + } + .order-status { + display: flex; + align-items: center; + } + .status-label { + font-size: 24rpx; + color: #9cf; + margin-right: 8rpx; + } + .status-value { + font-size: 24rpx; + color: #aaa; + } + .status-value.active { + color: #00ff80; + text-shadow: 0 0 10rpx #00ff80; + } + + /* ========== 核心资产卡 ========== */ + .asset-card { + display: flex; + justify-content: space-between; + align-items: center; + background: linear-gradient(135deg, #0A1F3A, #0B2A4A); + border: 2rpx solid #00a6ff; + clip-path: polygon(0 0, 100% 0, 95% 100%, 0 100%); + padding: 30rpx 30rpx; + margin-bottom: 30rpx; + box-shadow: 0 0 40rpx #00a6ff; + } + .asset-left { + flex: 1; + } + .asset-label { + font-size: 28rpx; + color: #9cf; + display: block; + margin-bottom: 10rpx; + } + .asset-value-group { + display: flex; + align-items: baseline; + } + .asset-number { + font-size: 68rpx; + font-weight: bold; + color: #fff; + text-shadow: 0 0 30rpx #00a6ff, 0 0 60rpx #00a6ff; + } + .asset-unit { + font-size: 32rpx; + color: #00a6ff; + margin-left: 10rpx; + } + .asset-right { + display: flex; + align-items: center; + background: rgba(255,255,255,0.1); + border: 2rpx solid #ffaa00; + clip-path: polygon(0 0, 100% 0, 90% 100%, 0 100%); + padding: 15rpx 30rpx; + position: relative; + overflow: hidden; + } + .withdraw-icon { + width: 50rpx; + height: 50rpx; + margin-right: 10rpx; + } + .withdraw-text { + font-size: 32rpx; + font-weight: bold; + color: #ffaa00; + } + .withdraw-glow { + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + animation: scan 2s infinite; + } + @keyframes scan { + 0% { left: -100%; } + 100% { left: 100%; } + } + .asset-right:active { + transform: scale(0.96); + } + + /* ========== 统计网格 ========== */ + .stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20rpx; + margin-bottom: 40rpx; + } + .stat-item { + background: rgba(0,30,50,0.7); + border: 1rpx solid #00a6ff; + clip-path: polygon(0 0, 100% 0, 90% 100%, 0 100%); + padding: 20rpx 10rpx; + text-align: center; + } + .stat-label { + font-size: 24rpx; + color: #9cf; + display: block; + margin-bottom: 10rpx; + } + .stat-value { + font-size: 32rpx; + font-weight: bold; + color: #fff; + } + + /* ========== 图形化数据(使用CSS变量) ========== */ + .graphic-panel { + background: rgba(0,30,50,0.6); + backdrop-filter: blur(15px); + border: 2rpx solid #00a6ff; + clip-path: polygon(0 0, 100% 0, 98% 100%, 0 100%); + padding: 30rpx; + margin-bottom: 40rpx; + display: flex; + justify-content: space-around; + flex-wrap: wrap; + } + .chart-item { + width: 45%; + text-align: center; + } + .chart-title { + font-size: 28rpx; + color: #9cf; + margin-bottom: 15rpx; + text-transform: uppercase; + border-left: 4rpx solid #00a6ff; + padding-left: 15rpx; + text-align: left; + } + + /* 环形进度条(使用CSS变量 --progress-angle) */ + .ring-chart { + position: relative; + width: 200rpx; + height: 200rpx; + margin: 0 auto; + border-radius: 50%; + overflow: hidden; + box-shadow: 0 0 40rpx #00a6ff; + } + .ring-progress { + width: 100%; + height: 100%; + background: conic-gradient(#00a6ff 0deg, #00a6ff var(--progress-angle), #1a3a5a var(--progress-angle), #1a3a5a 360deg); + border-radius: 50%; + } + .ring-inner { + position: absolute; + top: 15rpx; + left: 15rpx; + right: 15rpx; + bottom: 15rpx; + background: #0a1a2a; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + font-weight: bold; + color: #00a6ff; + box-shadow: inset 0 0 20rpx rgba(0,0,0,0.5); + } + + /* 提现进度条(使用CSS变量 --progress-width) */ + .progress-container { + height: 30rpx; + background: #1a3a5a; + border-radius: 15rpx; + position: relative; + overflow: hidden; + width: 100%; + margin-top: 20rpx; + } + .progress-bar { + height: 100%; + background: linear-gradient(90deg, #00a6ff, #80d4ff); + border-radius: 15rpx; + width: var(--progress-width); + position: relative; + } + .flow-bar::after { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent); + animation: flow 1.5s infinite; + } + @keyframes flow { + 0% { left: -100%; } + 100% { left: 100%; } + } + .progress-label { + position: absolute; + right: 20rpx; + top: -30rpx; + font-size: 24rpx; + color: #00a6ff; + } + .progress-stats { + display: flex; + justify-content: space-between; + font-size: 24rpx; + color: #9cf; + margin-top: 15rpx; + } + + /* ========== 功能按钮(图标强光效果) ========== */ + .function-panel { + margin-bottom: 40rpx; + } + .function-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20rpx; + } + .func-item { + background: rgba(0,20,40,0.8); + border: 1rpx solid #00a6ff; + clip-path: polygon(0 0, 100% 0, 90% 100%, 0 100%); + padding: 25rpx 0; + display: flex; + flex-direction: column; + align-items: center; + transition: 0.2s; + position: relative; + overflow: hidden; + } + .func-item:active { + transform: scale(0.96); + box-shadow: 0 0 40rpx #00a6ff; + } + /* 图标超强发光 */ + .super-glow { + width: 60rpx; + height: 60rpx; + margin-bottom: 15rpx; + filter: drop-shadow(0 0 25rpx #00a6ff) drop-shadow(0 0 50rpx #00a6ff) drop-shadow(0 0 80rpx #00a6ff); + animation: superGlow 1s infinite alternate; + } + @keyframes superGlow { + 0% { filter: drop-shadow(0 0 20rpx #00a6ff) drop-shadow(0 0 40rpx #00a6ff) drop-shadow(0 0 60rpx #00a6ff); } + 100% { filter: drop-shadow(0 0 30rpx #00a6ff) drop-shadow(0 0 60rpx #00a6ff) drop-shadow(0 0 100rpx #00a6ff); } + } + .func-label { + font-size: 26rpx; + color: #fff; + z-index: 1; + } + + /* ========== 加载遮罩 ========== */ + .loading-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(2,6,12,0.9); + backdrop-filter: blur(20px); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + } + .loading-core { + width: 200rpx; + height: 200rpx; + border: 4rpx solid #00a6ff; + border-radius: 50%; + border-top-color: transparent; + animation: spin 1s linear infinite; + display: flex; + align-items: center; + justify-content: center; + } + .loading-core::after { + content: ''; + width: 100rpx; + height: 100rpx; + border: 4rpx solid #ffaa00; + border-radius: 50%; + border-bottom-color: transparent; + animation: spin 1.5s reverse infinite; + } + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + + + + + /* ========== 原有样式(完全不变) ========== */ +/* ... 此处省略,切勿修改 ... */ + +/* ========== 联系管事胶囊(新增,适配机甲风格) ========== */ +.inviter-capsule { + margin: 30rpx 25rpx 30rpx; + background: rgba(0, 20, 40, 0.85); + backdrop-filter: blur(20px); + border: 2rpx solid #00a6ff; + clip-path: polygon(5% 0, 100% 0, 95% 100%, 0 100%); + padding: 20rpx 30rpx; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 0 30rpx rgba(0, 166, 255, 0.4); + position: relative; + z-index: 1; + } + + .capsule-body { + display: flex; + align-items: center; + flex: 1; + } + + .capsule-avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + border: 3rpx solid #00a6ff; + box-shadow: 0 0 20rpx #00a6ff; + margin-right: 20rpx; + background: #0a1a2a; + } + + .capsule-info { + display: flex; + flex-direction: column; + } + + .capsule-name { + font-size: 30rpx; + font-weight: bold; + color: #fff; + text-shadow: 0 0 15rpx #00a6ff; + margin-bottom: 6rpx; + } + + .capsule-uid { + font-size: 24rpx; + color: #9cf; + } + + .capsule-contact { + background: linear-gradient(135deg, #00a6ff, #0080ff); + border-radius: 30rpx; + padding: 12rpx 30rpx; + font-size: 26rpx; + font-weight: bold; + color: #fff; + box-shadow: 0 0 20rpx #00a6ff; + letter-spacing: 2rpx; + transition: all 0.3s; + } + + .capsule-contact:active { + transform: scale(0.95); + box-shadow: 0 0 40rpx #00a6ff; + } + + + + + + + + /* ========== 🆕 称号标签展示区 ========== */ +.badge-area { + width: 100%; + margin: 20rpx 0 30rpx 0; + padding: 0 20rpx; + box-sizing: border-box; + } + + .badge-scroll { + width: 100%; + max-height: 140rpx; + overflow-y: auto; + background: transparent; + } + + .badge-grid { + display: flex; + flex-wrap: wrap; + gap: 8rpx; + padding: 6rpx 0; + align-items: center; + justify-content: flex-start; + } + + .badge-item { + flex-shrink: 0; + } \ No newline at end of file diff --git a/pages/jisufd/jisufd.js b/pages/jisufd/jisufd.js new file mode 100644 index 0000000..49ae9cb --- /dev/null +++ b/pages/jisufd/jisufd.js @@ -0,0 +1,618 @@ +// pages/jisufd/jisufd.js +import request from '../../utils/request.js' + +const PAGE_SIZE = 50 +const LINK_PAGE_SIZE = 5 + +Page({ + data: { + sjyue: '0.00', + shangpinList: [], // 最终展示的商品类型列表(已倒序) + selectedTypeId: null, + selectedHuiyuanId: null, + selectedYaoqiuleixing: null, + currentTypeChenghaoList: [], + + searchParams: { keyword: '', minPrice: '', labelId: '' }, + searchLabelName: '', + isSearchMode: false, + + templateList: [], + currentPage: 1, + hasMoreData: true, + isLoadingMore: false, + + linkMap: {}, + + showDetailModal: false, + showAddModal: false, + showDeleteConfirm: false, + showNewLinkConfirm: false, + + currentTemplate: null, + currentTemplateIndex: -1, + + isEditing: false, + editJieshao: '', + editJiage: '', + editLabelId: '', + editLabelName: '', + editCommissionEnabled: false, + editCommissionValue: '', + + addJieshao: '', + addJiage: '', + addLabelId: '', + addLabelName: '', + addCommissionEnabled: false, + addCommissionValue: '', + + detailLinkFilter: 0, + detailLinkList: [], + detailLinkPage: 1, + detailLinkHasMore: true, + isLoadingLinks: false, + + isLoading: false + }, + + onLoad() { + this.loadShangjiaYue() + this.loadShangpinTypes() + }, + + onShow() { + this.registerNotificationComponent() + this.loadShangjiaYue() + }, + + registerNotificationComponent() { + const app = getApp() + const comp = this.selectComponent('#global-notification') + if (comp && comp.showNotification) { + app.globalData.globalNotification = { + show: data => comp.showNotification(data), + hide: () => comp.hideNotification() + } + } + }, + + loadShangjiaYue() { + const app = getApp() + const data = app.globalData.shangjia || {} + this.setData({ sjyue: data.sjyue || '0.00' }) + }, + + async loadShangpinTypes() { + this.setData({ isLoading: true }) + try { + const res = await request({ url: '/dingdan/sjspleixing', method: 'POST' }) + if (res && res.data.code === 200) { + let list = res.data.data || [] + // 强制倒序排列 + list.reverse() + this.setData({ shangpinList: list, isLoading: false }) + if (list.length > 0) { + this.setSelectedType(list[0]) + } + } + } catch (e) { + console.error(e) + this.setData({ isLoading: false }) + wx.showToast({ title: '加载类型失败', icon: 'error' }) + } + }, + + setSelectedType(item) { + this.setData({ + selectedTypeId: item.id, + selectedHuiyuanId: item.huiyuan_id, + selectedYaoqiuleixing: item.yaoqiuleixing, + currentTypeChenghaoList: item.chenghaoList || [] + }, () => { + this.resetAndLoadTemplates() + }) + }, + + resetAndLoadTemplates() { + this.setData({ + templateList: [], + currentPage: 1, + hasMoreData: true, + isSearchMode: false, + searchParams: { keyword: '', minPrice: '', labelId: '' }, + searchLabelName: '' + }) + this.loadTemplates() + }, + + async loadTemplates() { + const { selectedTypeId, currentPage, isSearchMode, searchParams, linkMap } = this.data + if (!selectedTypeId) { + wx.showToast({ title: '请先选择订单类型', icon: 'none' }) + return + } + if (currentPage === 1) this.setData({ isLoading: true }) + else this.setData({ isLoadingMore: true }) + + try { + const postData = { + shangpinTypeId: selectedTypeId, + page: currentPage, + pageSize: PAGE_SIZE, + getType: isSearchMode ? 1 : 2 + } + if (isSearchMode) { + if (searchParams.keyword) postData.keyword = searchParams.keyword.trim() + if (searchParams.minPrice) postData.minPrice = searchParams.minPrice + if (searchParams.labelId) postData.labelId = searchParams.labelId + } + + const res = await request({ url: '/peizhi/sjddmblb', method: 'POST', data: postData }) + if (res && res.data.code === 200) { + const data = res.data.data || {} + const newTemplates = data.list || [] + const existingIds = new Set(this.data.templateList.map(item => item.mobanId)) + const uniqueNew = newTemplates.filter(item => !existingIds.has(item.mobanId)) + + const processed = uniqueNew.map(item => { + const mobanId = item.mobanId || item.id + const cached = linkMap[mobanId] || {} + return { + ...item, + hasGenerated: cached.hasGenerated || !!item.linkUrl, + isCopied: cached.isCopied || false, + linkUrl: cached.linkUrl || item.linkUrl || '', + mobanId: mobanId, + jieshao: item.jieshao || item.moban_jieshao, + jiage: item.jiage || item.price, + fabushuliang: item.fabushuliang || item.fabu_shuliang || 0, + labelId: item.labelId, + labelName: item.labelName || '', + commissionEnabled: item.commissionEnabled, + commissionValue: item.commissionValue + } + }) + + this.setData({ + templateList: currentPage === 1 ? processed : [...this.data.templateList, ...processed], + hasMoreData: data.hasMore !== false && processed.length === PAGE_SIZE, + isLoading: false, + isLoadingMore: false + }) + } else { + this.setData({ isLoading: false, isLoadingMore: false, hasMoreData: false }) + } + } catch (e) { + console.error(e) + this.setData({ isLoading: false, isLoadingMore: false }) + wx.showToast({ title: '加载模板失败', icon: 'error' }) + } + }, + + // ---------- 搜索相关 ---------- + onSearchKeywordInput(e) { this.setData({ 'searchParams.keyword': e.detail.value }) }, + onSearchMinPriceInput(e) { this.setData({ 'searchParams.minPrice': e.detail.value }) }, + onSearchLabelChange(e) { + const idx = e.detail.value + const label = this.data.currentTypeChenghaoList[idx] + this.setData({ + 'searchParams.labelId': label ? label.id : '', + searchLabelName: label ? label.mingcheng : '' + }) + }, + onSearchConfirm() { + const { searchParams } = this.data + if (!searchParams.keyword && !searchParams.minPrice && !searchParams.labelId) { + this.resetAndLoadTemplates() + return + } + this.setData({ + isSearchMode: true, + currentPage: 1, + templateList: [], + hasMoreData: true + }, () => this.loadTemplates()) + }, + onClearSearch() { this.resetAndLoadTemplates() }, + + // ---------- 卡片生成链接 ---------- + async onGenerateLink(e) { + const { id } = e.currentTarget.dataset + const idx = this.data.templateList.findIndex(item => item.mobanId === id) + if (idx === -1) return + await this.generateLinkForTemplate(this.data.templateList[idx], idx) + }, + + onCopyLink(e) { + const { id } = e.currentTarget.dataset + const idx = this.data.templateList.findIndex(item => item.mobanId === id) + if (idx === -1) return + const template = this.data.templateList[idx] + if (!template.linkUrl) { + wx.showToast({ title: '请先生成链接', icon: 'none' }) + return + } + wx.setClipboardData({ + data: template.linkUrl, + success: () => { + const { linkMap } = this.data + const mobanId = template.mobanId + const updatedLinkMap = { ...linkMap } + if (updatedLinkMap[mobanId]) updatedLinkMap[mobanId].isCopied = true + const updatedList = [...this.data.templateList] + updatedList[idx].isCopied = true + this.setData({ linkMap: updatedLinkMap, templateList: updatedList }) + wx.showToast({ title: '链接已复制', icon: 'success' }) + } + }) + }, + + async generateLinkForTemplate(template, templateIndex) { + const { selectedTypeId, linkMap } = this.data + if (!selectedTypeId) { + wx.showToast({ title: '请选择订单类型', icon: 'none' }) + return + } + wx.showLoading({ title: '生成中...', mask: true }) + try { + const res = await request({ + url: '/peizhi/sjljhq', + method: 'POST', + data: { mobanId: template.mobanId, shangpinTypeId: selectedTypeId } + }) + if (res && res.data.code === 200) { + const data = res.data.data || {} + const mobanId = template.mobanId + const updatedLinkMap = { ...linkMap } + updatedLinkMap[mobanId] = { + hasGenerated: true, + linkUrl: data.linkUrl || '', + isCopied: false, + newBalance: data.newBalance + } + const updatedList = [...this.data.templateList] + updatedList[templateIndex] = { + ...updatedList[templateIndex], + hasGenerated: true, + linkUrl: data.linkUrl || '', + isCopied: false, + newBalance: data.newBalance + } + if (data.newBalance) { + const app = getApp() + if (app.globalData.shangjia) app.globalData.shangjia.sjyue = data.newBalance + this.setData({ sjyue: data.newBalance }) + } + this.setData({ linkMap: updatedLinkMap, templateList: updatedList }) + wx.showToast({ title: '链接生成成功', icon: 'success' }) + } else { + wx.showToast({ title: res.data.msg || '生成失败', icon: 'error' }) + } + } catch (e) { + wx.showToast({ title: '生成失败', icon: 'error' }) + } finally { + wx.hideLoading() + } + }, + + onCardTap(e) { + const { target } = e + if (target.className && (target.className.includes('btn') || target.className.includes('action'))) return + const { item } = e.currentTarget.dataset + if (!item) return + const idx = this.data.templateList.findIndex(t => t.mobanId === item.mobanId) + this.setData({ + currentTemplate: item, + currentTemplateIndex: idx, + showDetailModal: true, + isEditing: false, + editJieshao: item.jieshao || '', + editJiage: item.jiage || '', + editLabelId: item.labelId || '', + editLabelName: item.labelName || '', + editCommissionEnabled: item.commissionEnabled || false, + editCommissionValue: item.commissionValue || '', + detailLinkFilter: 0, + detailLinkList: [], + detailLinkPage: 1, + detailLinkHasMore: true + }) + this.loadDetailLinks() + }, + + hideDetailModal() { this.setData({ showDetailModal: false, isEditing: false }) }, + showAddModal() { + this.setData({ + showAddModal: true, + addJieshao: '', addJiage: '', addLabelId: '', addLabelName: '', + addCommissionEnabled: false, addCommissionValue: '' + }) + }, + hideAddModal() { this.setData({ showAddModal: false }) }, + hideDeleteConfirm() { this.setData({ showDeleteConfirm: false }) }, + hideNewLinkConfirm() { this.setData({ showNewLinkConfirm: false }) }, + + // ---------- 链接筛选与加载 ---------- + onToggleLinkFilter() { + const newFilter = this.data.detailLinkFilter === 0 ? 1 : 0 + this.setData({ + detailLinkFilter: newFilter, + detailLinkList: [], + detailLinkPage: 1, + detailLinkHasMore: true + }, () => this.loadDetailLinks()) + }, + + async loadDetailLinks() { + const { currentTemplate, detailLinkFilter, detailLinkPage, isLoadingLinks } = this.data + if (!currentTemplate || isLoadingLinks) return + this.setData({ isLoadingLinks: true }) + try { + const res = await request({ + url: '/peizhi/hqsjlslj', + method: 'POST', + data: { + mobanId: currentTemplate.mobanId, + used: detailLinkFilter, + page: detailLinkPage, + pageSize: LINK_PAGE_SIZE + } + }) + if (res && res.data.code === 200) { + const data = res.data.data || {} + const newLinks = data.list || [] + const merged = detailLinkPage === 1 ? newLinks : [...this.data.detailLinkList, ...newLinks] + this.setData({ + detailLinkList: merged, + detailLinkHasMore: newLinks.length === LINK_PAGE_SIZE, + isLoadingLinks: false + }) + } else { + this.setData({ isLoadingLinks: false }) + } + } catch (e) { + this.setData({ isLoadingLinks: false }) + wx.showToast({ title: '加载链接失败', icon: 'error' }) + } + }, + + onLoadMoreLinks() { + if (!this.data.detailLinkHasMore || this.data.isLoadingLinks) return + this.setData({ detailLinkPage: this.data.detailLinkPage + 1 }, () => this.loadDetailLinks()) + }, + + onCopyLinkItem(e) { + const url = e.currentTarget.dataset.url + if (!url) return + wx.setClipboardData({ data: url, success: () => wx.showToast({ title: '已复制', icon: 'success' }) }) + }, + + // ---------- 弹窗内操作 ---------- + onGenerateLinkModal() { + const { currentTemplate, currentTemplateIndex } = this.data + if (!currentTemplate) return + if (currentTemplate.hasGenerated && !currentTemplate.isCopied) { + this.setData({ showNewLinkConfirm: true }) + return + } + this.generateLinkForTemplate(currentTemplate, currentTemplateIndex) + }, + + onCopyLinkModal() { + const { currentTemplate, linkMap } = this.data + if (!currentTemplate || !currentTemplate.linkUrl) { + wx.showToast({ title: '请先生成链接', icon: 'none' }) + return + } + wx.setClipboardData({ + data: currentTemplate.linkUrl, + success: () => { + const mobanId = currentTemplate.mobanId + const updatedLinkMap = { ...linkMap } + if (updatedLinkMap[mobanId]) updatedLinkMap[mobanId].isCopied = true + const updatedList = [...this.data.templateList] + if (this.data.currentTemplateIndex !== -1) { + updatedList[this.data.currentTemplateIndex].isCopied = true + } + const updatedCurrent = { ...currentTemplate, isCopied: true } + this.setData({ linkMap: updatedLinkMap, templateList: updatedList, currentTemplate: updatedCurrent }) + wx.showToast({ title: '链接已复制', icon: 'success' }) + } + }) + }, + + // ---------- 编辑 ---------- + onEditTemplate() { + if (!this.data.isEditing) this.setData({ isEditing: true }) + else this.saveTemplateEdit() + }, + onEditJieshaoInput(e) { this.setData({ editJieshao: e.detail.value }) }, + onEditJiageInput(e) { this.setData({ editJiage: e.detail.value }) }, + onEditLabelChange(e) { + const idx = e.detail.value + const label = this.data.currentTypeChenghaoList[idx] + this.setData({ editLabelId: label ? label.id : '', editLabelName: label ? label.mingcheng : '' }) + }, + onEditCommissionToggle() { this.setData({ editCommissionEnabled: !this.data.editCommissionEnabled }) }, + onEditCommissionValueInput(e) { this.setData({ editCommissionValue: e.detail.value }) }, + + async saveTemplateEdit() { + const { currentTemplate, selectedTypeId, editJieshao, editJiage, editLabelId, editCommissionEnabled, editCommissionValue } = this.data + if (!currentTemplate || !selectedTypeId) return + if (editCommissionEnabled && (!editCommissionValue || isNaN(parseFloat(editCommissionValue)))) { + wx.showToast({ title: '请输入有效佣金金额', icon: 'none' }) + return + } + wx.showLoading({ title: '保存中...', mask: true }) + try { + const res = await request({ + url: '/peizhi/sjddmbgx', + method: 'POST', + data: { + mobanId: currentTemplate.mobanId, + shangpinTypeId: selectedTypeId, + jieshao: editJieshao, + jiage: editJiage, + labelId: editLabelId || '', + commissionEnabled: editCommissionEnabled, + commissionValue: editCommissionValue + } + }) + if (res && res.data.code === 200) { + const updatedList = [...this.data.templateList] + const idx = this.data.currentTemplateIndex + if (idx !== -1) { + updatedList[idx] = { + ...updatedList[idx], + jieshao: editJieshao, + jiage: editJiage, + labelId: editLabelId, + labelName: this.getLabelNameById(editLabelId), + commissionEnabled: editCommissionEnabled, + commissionValue: editCommissionValue + } + this.setData({ templateList: updatedList, currentTemplate: updatedList[idx], isEditing: false }) + } + wx.showToast({ title: '修改成功', icon: 'success' }) + } else { + wx.showToast({ title: res.data.msg || '修改失败', icon: 'error' }) + } + } catch (e) { + wx.showToast({ title: '修改失败', icon: 'error' }) + } finally { wx.hideLoading() } + }, + + onDeleteTemplate() { this.setData({ showDeleteConfirm: true }) }, + + async confirmDeleteTemplate() { + const { currentTemplate, selectedTypeId, linkMap } = this.data + if (!currentTemplate || !selectedTypeId) return + wx.showLoading({ title: '删除中...', mask: true }) + try { + const res = await request({ + url: '/peizhi/sjddmbsc', + method: 'POST', + data: { mobanId: currentTemplate.mobanId, shangpinTypeId: selectedTypeId } + }) + if (res && res.data.code === 200) { + const updatedLinkMap = { ...linkMap } + delete updatedLinkMap[currentTemplate.mobanId] + const updatedList = [...this.data.templateList] + updatedList.splice(this.data.currentTemplateIndex, 1) + this.setData({ + linkMap: updatedLinkMap, + templateList: updatedList, + showDetailModal: false, + showDeleteConfirm: false, + currentTemplate: null, + currentTemplateIndex: -1 + }) + wx.showToast({ title: '删除成功', icon: 'success' }) + } else { + wx.showToast({ title: res.data.msg || '删除失败', icon: 'error' }) + } + } catch (e) { + wx.showToast({ title: '删除失败', icon: 'error' }) + } finally { wx.hideLoading() } + }, + + async confirmNewLink() { + const { currentTemplate, currentTemplateIndex } = this.data + if (!currentTemplate) return + this.setData({ showNewLinkConfirm: false }) + await this.generateLinkForTemplate(currentTemplate, currentTemplateIndex) + }, + + // ---------- 添加模板 ---------- + onAddJieshaoInput(e) { this.setData({ addJieshao: e.detail.value }) }, + onAddJiageInput(e) { this.setData({ addJiage: e.detail.value }) }, + onAddLabelChange(e) { + const idx = e.detail.value + const label = this.data.currentTypeChenghaoList[idx] + this.setData({ addLabelId: label ? label.id : '', addLabelName: label ? label.mingcheng : '' }) + }, + onAddCommissionToggle() { this.setData({ addCommissionEnabled: !this.data.addCommissionEnabled }) }, + onAddCommissionValueInput(e) { this.setData({ addCommissionValue: e.detail.value }) }, + + async onAddTemplate() { + const { selectedTypeId, addJieshao, addJiage, addLabelId, addCommissionEnabled, addCommissionValue } = this.data + if (!selectedTypeId) { + wx.showToast({ title: '请选择订单类型', icon: 'none' }) + return + } + if (!addJieshao.trim()) { + wx.showToast({ title: '请输入订单介绍', icon: 'none' }) + return + } + if (!addJiage || isNaN(parseFloat(addJiage)) || parseFloat(addJiage) <= 0) { + wx.showToast({ title: '请输入有效的价格', icon: 'none' }) + return + } + if (addCommissionEnabled && (!addCommissionValue || isNaN(parseFloat(addCommissionValue)))) { + wx.showToast({ title: '请输入有效的佣金金额', icon: 'none' }) + return + } + + wx.showLoading({ title: '添加中...', mask: true }) + try { + const res = await request({ + url: '/peizhi/sjtjddmb', + method: 'POST', + data: { + shangpinTypeId: selectedTypeId, + jieshao: addJieshao.trim(), + jiage: addJiage, + labelId: addLabelId || '', + commissionEnabled: addCommissionEnabled, + commissionValue: addCommissionValue + } + }) + if (res && res.data.code === 200) { + const data = res.data.data || {} + const newTemplate = { + mobanId: data.mobanId, + jieshao: addJieshao, + jiage: addJiage, + fabushuliang: 0, + hasGenerated: false, + isCopied: false, + linkUrl: '', + labelId: addLabelId, + labelName: this.getLabelNameById(addLabelId), + commissionEnabled: addCommissionEnabled, + commissionValue: addCommissionValue + } + this.setData({ + templateList: [newTemplate, ...this.data.templateList], + showAddModal: false, + addJieshao: '', addJiage: '', addLabelId: '', addLabelName: '', + addCommissionEnabled: false, addCommissionValue: '' + }) + wx.showToast({ title: '添加成功', icon: 'success' }) + } else { + wx.showToast({ title: res.data.msg || '添加失败', icon: 'error' }) + } + } catch (e) { + wx.showToast({ title: '添加失败', icon: 'error' }) + } finally { wx.hideLoading() } + }, + + getLabelNameById(labelId) { + if (!labelId) return '' + const list = this.data.currentTypeChenghaoList + const found = list.find(item => item.id == labelId) + return found ? found.mingcheng : '' + }, + + onScrollToLower() { + const { hasMoreData, isLoadingMore, isSearchMode } = this.data + if (!hasMoreData || isLoadingMore || isSearchMode) return + this.setData({ currentPage: this.data.currentPage + 1 }, () => this.loadTemplates()) + }, + + onSelectType(e) { + const { id, item } = e.currentTarget.dataset + this.setSelectedType(item) + } +}) \ No newline at end of file diff --git a/pages/jisufd/jisufd.json b/pages/jisufd/jisufd.json new file mode 100644 index 0000000..e0c0695 --- /dev/null +++ b/pages/jisufd/jisufd.json @@ -0,0 +1,12 @@ +{ + "usingComponents": { + "global-notification": "/components/global-notification/global-notification", + "chenghao-tag": "/components/chenghao-tag/chenghao-tag" + }, + "backgroundTextStyle": "dark", + "navigationBarBackgroundColor": "#0a0a0f", + "navigationBarTitleText": "极速派单", + "navigationBarTextStyle": "white", + "backgroundColor": "#0a0a0f", + "enablePullDownRefresh": false +} \ No newline at end of file diff --git a/pages/jisufd/jisufd.wxml b/pages/jisufd/jisufd.wxml new file mode 100644 index 0000000..5d0fc2a --- /dev/null +++ b/pages/jisufd/jisufd.wxml @@ -0,0 +1,230 @@ + + + + + 可用余额 + + {{sjyue}} + + + 生成链接将从此余额扣除 + + + + + 选择订单类型 + + + + {{item.jieshao}} + + + + + + + + + 🔍 + + + 搜索 + + + + + 金额≥ + + + + 标签 + + + {{searchLabelName || '全部'}} + + + + 清空 + + + + + + + + + + + {{item.jieshao || '无介绍'}} + + + + + + 价格: + {{item.jiage || '0.00'}}元 + + + + {{item.hasGenerated ? '获取新链接' : '生成链接'}} + + + {{item.isCopied ? '已复制' : '一键复制'}} + + + + + + + 添加模板 + 点击创建新模板 + + + ... + 没有更多模板了 + 暂无模板 + + + + + + + + × + 模板详情 + + + 订单介绍 + {{currentTemplate.jieshao || '无介绍'}} + + + 关联标签 + + + + 价格{{currentTemplate.jiage || '0.00'}}元 + | + 发布数量{{currentTemplate.fabushuliang || 0}}次 + | + 模板ID{{currentTemplate.mobanId || 'N/A'}} + + + 佣金要求 + {{currentTemplate.commissionValue}}元(建议≤50) + + + + 链接状态: + 未使用 + 已使用 + + + + + {{item.lianjie}} + {{item.is_used ? '已使用' : '未使用'}} + 复制 + + + + 获取更多链接 + + 加载中... + 暂无链接 + + + + {{currentTemplate.hasGenerated ? '获取新链接' : '生成链接'}} + {{currentTemplate.isCopied ? '已复制' : '复制链接'}} + {{isEditing ? '保存修改' : '立即修改'}} + 删除模板 + + + + + 订单介绍 + + {{countdown}}秒后可确认 + + 取消 + 确认拒绝 + + + + + + + + 确认转移考核 + 转移后,您将无法继续考核此订单,可在考核中心重新接待。 + {{countdown}}秒后可确认 + + 取消 + 确认转移 + + + + + \ No newline at end of file diff --git a/pages/kaohe_dafen/kaohe_dafen.wxss b/pages/kaohe_dafen/kaohe_dafen.wxss new file mode 100644 index 0000000..f736716 --- /dev/null +++ b/pages/kaohe_dafen/kaohe_dafen.wxss @@ -0,0 +1,170 @@ +/* 根容器,全屏且底部固定 */ +.root-page { + width: 100vw; + height: 100vh; + background: radial-gradient(ellipse at 20% 20%, #1a1e2b 0%, #0b0c12 100%); + display: flex; + flex-direction: column; + } + + /* 滚动区域 */ + .scroll-container { + flex: 1; + overflow-y: auto; + padding: 30rpx 30rpx 0; + padding-bottom: constant(safe-area-inset-bottom); + padding-bottom: env(safe-area-inset-bottom); + } + + /* 卡片 */ + .card { + background: rgba(25, 27, 36, 0.8); + backdrop-filter: blur(20px); + border: 1rpx solid rgba(201, 169, 88, 0.15); + border-radius: 24rpx; + padding: 30rpx; + margin-bottom: 30rpx; + box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.5); + } + + /* 用户信息 */ + .user-row { + display: flex; + align-items: center; + margin-bottom: 30rpx; + } + .avatar { + width: 80rpx; height: 80rpx; + border-radius: 50%; + border: 2rpx solid rgba(201,169,88,0.4); + margin-right: 20rpx; + } + .info { flex: 1; } + .uid { font-size: 30rpx; color: #f0f2ff; font-weight: 600; } + .time { font-size: 22rpx; color: #6a7090; margin-top: 4rpx; } + .contact-btn { + background: rgba(201,169,88,0.15); + border: 1rpx solid rgba(201,169,88,0.3); + border-radius: 20rpx; + padding: 8rpx 24rpx; + font-size: 24rpx; + color: #c9a558; + } + + /* 详情网格 */ + .detail-grid { + border-bottom: 1rpx solid rgba(255,255,255,0.06); + margin-bottom: 20rpx; + } + .detail-item { + display: flex; + align-items: center; + padding: 16rpx 0; + border-bottom: 1rpx solid rgba(255,255,255,0.04); + } + .detail-item:last-child { border-bottom: none; } + .label { font-size: 26rpx; color: #8a90b0; width: 160rpx; flex-shrink: 0; } + .value { font-size: 26rpx; color: #d0d4ff; font-weight: 500; } + .rule-content { white-space: pre-wrap; line-height: 1.5; } + .jinpai-note { font-size: 24rpx; color: #c9a558; font-weight: 600; margin-left: 10rpx; } + + /* 失败原因 */ + .fail-box { + background: rgba(255, 70, 70, 0.1); + border-left: 4rpx solid #ff5e5e; + padding: 16rpx; + border-radius: 8rpx; + margin-bottom: 20rpx; + } + .fail-label { font-size: 24rpx; color: #ff8a8a; } + .fail-text { font-size: 24rpx; color: #ffd0d0; } + + /* 昵称修改 */ + .nick-row { + display: flex; + align-items: center; + margin-bottom: 30rpx; + } + .nick-input { + flex: 1; + background: rgba(255,255,255,0.06); + border: 1rpx solid rgba(255,255,255,0.1); + border-radius: 12rpx; + padding: 12rpx 20rpx; + color: #fff; + font-size: 26rpx; + margin-right: 20rpx; + } + .save-nick-btn { + background: #c9a558; + color: #0b0c12; + padding: 12rpx 30rpx; + border-radius: 30rpx; + font-size: 26rpx; + font-weight: 600; + } + + /* 操作按钮 */ + .action-row { + display: flex; + gap: 20rpx; + margin-top: 20rpx; + } + .action-btn { + flex: 1; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 40rpx; + font-size: 28rpx; + font-weight: 600; + color: #fff; + } + .agree { background: linear-gradient(135deg, #c9a558, #9e7d2e); } + .reject { background: linear-gradient(135deg, #a85050, #802a2a); } + .transfer { background: linear-gradient(135deg, #4a6090, #2a3a60); } + + /* 加载/空状态 */ + .loading, .no-more, .empty { text-align: center; padding: 30rpx; color: #6a7090; font-size: 28rpx; } + + /* 弹窗 */ + .modal-mask { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + .modal-box { + width: 80%; + background: #1e2030; + border-radius: 24rpx; + padding: 30rpx; + border: 1rpx solid rgba(201,169,88,0.2); + } + .modal-header { font-size: 32rpx; color: #f0f2ff; font-weight: 600; margin-bottom: 20rpx; } + .modal-body { font-size: 26rpx; color: #b0b8d0; margin-bottom: 20rpx; } + .reason-input { + width: 100%; + height: 200rpx; + background: rgba(255,255,255,0.05); + border-radius: 12rpx; + padding: 15rpx; + color: #fff; + font-size: 26rpx; + box-sizing: border-box; + margin-bottom: 20rpx; + } + .countdown-text { text-align: center; font-size: 28rpx; color: #c9a558; margin-bottom: 20rpx; } + .modal-footer { display: flex; gap: 20rpx; } + .btn { + flex: 1; height: 80rpx; border-radius: 40rpx; + display: flex; align-items: center; justify-content: center; + font-size: 28rpx; font-weight: 600; + } + .cancel { background: #2a2f3e; color: #fff; } + .confirm { background: linear-gradient(135deg, #c9a558, #9e7d2e); color: #0b0c12; } + .confirm.disabled { opacity: 0.4; pointer-events: none; } \ No newline at end of file diff --git a/pages/kaohe_jilu/kaohe_jilu.js b/pages/kaohe_jilu/kaohe_jilu.js new file mode 100644 index 0000000..545e567 --- /dev/null +++ b/pages/kaohe_jilu/kaohe_jilu.js @@ -0,0 +1,149 @@ +// pages/kaohe_jilu/kaohe_jilu.js +const app = getApp(); +import request from '../../utils/request.js'; + +Page({ + data: { + // 板块筛选 + moduleList: [], + xuanzhongBankuaiId: 0, // 0代表全部 + // 状态筛选:0-全部 1-审核中 2-已通过 3-未通过 + statusFilter: 0, + // 记录列表 + recordList: [], + page: 1, + pageSize: 5, + hasMore: true, + isLoading: false, + isLoadingMore: false, + // 展开的记录ID集合(字符串数组) + expandedIds: [], + // OSS地址 + ossImageUrl: app.globalData.ossImageUrl || '', + }, + + onLoad() { + this.loadModules(); + }, + + // 加载板块(复用审核大厅接口) + async loadModules() { + try { + const res = await request({ url: '/dengji/shdthqmk', method: 'POST' }); + if (res && res.data.code === 0) { + const modules = res.data.data.modules || []; + this.setData({ moduleList: modules, xuanzhongBankuaiId: 0 }); + this.loadRecords(true); + } else { + wx.showToast({ title: '加载模块失败', icon: 'none' }); + } + } catch (e) { + wx.showToast({ title: '网络错误', icon: 'none' }); + } + }, + + // 选择板块 + selectModule(e) { + const bankuaiId = e.currentTarget.dataset.id; + if (bankuaiId === this.data.xuanzhongBankuaiId) return; + this.setData({ + xuanzhongBankuaiId: bankuaiId, + recordList: [], + page: 1, + hasMore: true, + expandedIds: [] + }); + this.loadRecords(true); + }, + + // 选择状态 + selectStatus(e) { + const status = e.currentTarget.dataset.status; + if (status === this.data.statusFilter) return; + this.setData({ + statusFilter: status, + recordList: [], + page: 1, + hasMore: true, + expandedIds: [] + }); + this.loadRecords(true); + }, + + // 加载历史记录 + async loadRecords(isRefresh = false) { + if (this.data.isLoading) return; + const page = isRefresh ? 1 : this.data.page; + if (!isRefresh && !this.data.hasMore) return; + + this.setData({ isLoading: true, isLoadingMore: !isRefresh }); + + try { + const res = await request({ + url: '/dengji/khghqkhjl', + method: 'POST', + data: { + bankuai_id: this.data.xuanzhongBankuaiId || undefined, + status: this.data.statusFilter || undefined, + page: page, + page_size: this.data.pageSize + } + }); + + this.setData({ isLoading: false, isLoadingMore: false }); + + if (res && res.data.code === 0) { + const list = res.data.data.list || []; + const hasMore = res.data.data.has_more; + // 拼接头像 + const oss = this.data.ossImageUrl; + const processedList = list.map(item => { + let avatar = item.shenqingren_avatar || ''; + if (avatar && !avatar.startsWith('http')) { + avatar = oss + avatar; + } + return { ...item, shenqingren_avatar: avatar }; + }); + const newList = isRefresh ? processedList : [...this.data.recordList, ...processedList]; + this.setData({ recordList: newList, page: page + 1, hasMore: hasMore }); + } else { + wx.showToast({ title: '加载记录失败', icon: 'none' }); + } + } catch (e) { + this.setData({ isLoading: false, isLoadingMore: false }); + wx.showToast({ title: '网络错误', icon: 'none' }); + } + }, + + // 上拉加载更多 + onReachBottom() { + if (this.data.hasMore && !this.data.isLoading && !this.data.isLoadingMore) { + this.loadRecords(false); + } + }, + + // 展开/收起详情 + toggleExpand(e) { + const jiluId = e.currentTarget.dataset.id; + // 确保 jiluId 是字符串 + const id = String(jiluId); + const expandedIds = this.data.expandedIds.slice(); // 拷贝 + const index = expandedIds.indexOf(id); + if (index > -1) { + expandedIds.splice(index, 1); + } else { + expandedIds.push(id); + } + this.setData({ expandedIds }); + }, + + // 去评判(跳转考核打分页) + goJudge(e) { + const item = e.currentTarget.dataset.item; + if (!item) return; + wx.navigateTo({ url: '/pages/kaohe_dafen/kaohe_dafen?jilu_id=' + item.jilu_id }); + }, + + // 阻止冒泡 + noop() {} +}); \ No newline at end of file diff --git a/pages/kaohe_jilu/kaohe_jilu.json b/pages/kaohe_jilu/kaohe_jilu.json new file mode 100644 index 0000000..f6a68c8 --- /dev/null +++ b/pages/kaohe_jilu/kaohe_jilu.json @@ -0,0 +1,9 @@ +{ + "usingComponents": { + "chenghao-tag": "/components/chenghao-tag/chenghao-tag", + "global-notification": "/components/global-notification/global-notification" + }, + "navigationBarTitleText": "考核记录", + "navigationBarBackgroundColor": "#FDFBF7", + "navigationBarTextStyle": "black" + } \ No newline at end of file diff --git a/pages/kaohe_jilu/kaohe_jilu.wxml b/pages/kaohe_jilu/kaohe_jilu.wxml new file mode 100644 index 0000000..b8ac9ff --- /dev/null +++ b/pages/kaohe_jilu/kaohe_jilu.wxml @@ -0,0 +1,130 @@ + + + + + + + + + 全部板块 + + + + {{item.mingcheng}} + + + + + + + 全部 + 审核中 + 已通过 + 未通过 + + + + + + + + 暂无考核记录 + + + + + + + + + {{item.zhuangtai == 0 ? '审核中' : (item.zhuangtai == 1 ? '已通过' : (item.zhuangtai == 2 ? '未通过' : '待审核'))}} + + + + + + + 板块 + {{item.bankuai_mingcheng}} + + + 标签 + + + + 次数 + {{item.cishu}}次 + + + 总费用 + ¥{{item.total_fee}} + + + + + + + {{expandedIds.indexOf(item.jilu_id) > -1 ? '收起详情' : '展开详情'}} + + + 去评判 + + + + + + + 考核规则 + {{item.guize_neirong}} + + + 缴纳记录 + + 第{{item.cishu}}次:¥{{item.jine}} + + 暂无缴纳记录 + + + 拒绝记录 + + + {{item.create_time}} + 第{{item.cishu}}次拒绝:{{item.yuanyin}} + + + 暂无拒绝记录 + + + 其他信息 + + 打手游戏ID:{{item.dashou_youxi_id || '无'}} + 打手备注:{{item.dashou_beizhu || '无'}} + 审核官游戏ID:{{item.shenheguan_youxi_id || '无'}} + 创建时间:{{item.create_time}} + 更新时间:{{item.update_time}} + + + + + + + 加载更多... + —— 没有更多了 —— + + + + + + + + 加载中... + + + + \ No newline at end of file diff --git a/pages/kaohe_jilu/kaohe_jilu.wxss b/pages/kaohe_jilu/kaohe_jilu.wxss new file mode 100644 index 0000000..bf37003 --- /dev/null +++ b/pages/kaohe_jilu/kaohe_jilu.wxss @@ -0,0 +1,263 @@ +/* pages/kaohe_jilu/kaohe_jilu.wxss - 晨露微光风格(左右均等边距) */ +page { + background: linear-gradient(135deg, #FDFBF7 0%, #F2EFE9 100%); + color: #4A4A4A; + font-family: 'SF Pro Display', 'PingFang SC', system-ui; + height: 100vh; + overflow: hidden; + } + + .page-root { + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; + } + + /* 筛选栏 */ + .filter-bar { + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(20px); + border-bottom: 1rpx solid rgba(0,0,0,0.04); + flex-shrink: 0; + padding: 20rpx 0 0; + } + + .module-scroll { + white-space: nowrap; + height: 70rpx; + padding: 0 30rpx; + } + .module-container { + display: inline-flex; + gap: 20rpx; + align-items: center; + height: 70rpx; + } + + .filter-item { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 30rpx; + height: 56rpx; + border-radius: 28rpx; + background: #F0EFEC; + font-size: 26rpx; + color: #6B6B6B; + transition: all 0.2s; + } + .filter-item.active { + background: #C8A27A; + color: #FFFFFF; + font-weight: 600; + box-shadow: 0 4rpx 12rpx rgba(200, 162, 122, 0.3); + } + + .status-row { + display: flex; + padding: 15rpx 30rpx 20rpx; + gap: 30rpx; + } + .status-item { + font-size: 26rpx; + color: #8B8B8B; + padding-bottom: 8rpx; + border-bottom: 4rpx solid transparent; + transition: all 0.2s; + } + .status-item.active { + color: #C8A27A; + border-bottom-color: #C8A27A; + font-weight: 600; + } + + /* 列表容器 */ + .list-container { + flex: 1; + overflow: hidden; + } + .list-scroll { + height: 100%; + padding: 20rpx 0; /* 上下内边距 */ + } + + .empty-tip { + text-align: center; + color: #A0A0A0; + font-size: 28rpx; + padding-top: 200rpx; + } + + /* 卡片:统一左右边距 30rpx */ + .card { + background: #FFFFFF; + border-radius: 28rpx; + padding: 30rpx; + margin: 0 30rpx 24rpx 30rpx; /* 左右严格相等 */ + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.04); + transition: all 0.3s; + } + + .card-header { + display: flex; + align-items: center; + margin-bottom: 24rpx; + } + .avatar { + width: 72rpx; + height: 72rpx; + border-radius: 50%; + margin-right: 18rpx; + border: 2rpx solid #F0EDE8; + background: #F7F5F2; + } + .user-info { + flex: 1; + } + .nickname { + font-size: 30rpx; + font-weight: 600; + color: #3A3A3A; + display: block; + } + .uid { + font-size: 24rpx; + color: #9A9A9A; + margin-top: 4rpx; + } + .status-tag { + font-size: 24rpx; + padding: 6rpx 18rpx; + border-radius: 20rpx; + font-weight: 500; + } + .status-0 { background: #FFF3E0; color: #EF6C00; } + .status-1 { background: #E8F5E9; color: #2E7D32; } + .status-2 { background: #FFEBEE; color: #C62828; } + .status-3 { background: #E3F2FD; color: #1565C0; } + + .card-summary { + display: flex; + flex-wrap: wrap; + gap: 16rpx 24rpx; + margin-bottom: 20rpx; + } + .summary-item { + display: flex; + align-items: center; + font-size: 26rpx; + } + .summary-item .label { + color: #8B8B8B; + margin-right: 12rpx; + } + .summary-item .value { + color: #3A3A3A; + font-weight: 500; + } + + .card-actions { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 20rpx; + border-top: 1rpx solid #F5F2ED; + } + .expand-btn { + font-size: 26rpx; + color: #C8A27A; + font-weight: 500; + } + .judge-btn { + font-size: 26rpx; + background: #C8A27A; + color: #FFFFFF; + padding: 8rpx 24rpx; + border-radius: 24rpx; + font-weight: 500; + } + + /* 展开详情(左右边距继承卡片,无需额外设置) */ + .card-detail { + margin-top: 20rpx; + padding-top: 20rpx; + border-top: 1rpx solid #F5F2ED; + } + .detail-section { + margin-bottom: 20rpx; + } + .detail-title { + font-size: 26rpx; + font-weight: 600; + color: #5A5A5A; + margin-bottom: 10rpx; + display: block; + } + .detail-text { + font-size: 24rpx; + color: #6B6B6B; + line-height: 1.6; + } + .fee-row, .reject-row { + font-size: 24rpx; + color: #6B6B6B; + padding: 6rpx 0; + } + .reject-item { + display: flex; + flex-direction: column; + background: #FFF9F5; + padding: 12rpx 16rpx; + border-radius: 12rpx; + margin-bottom: 12rpx; + } + .reject-time { + font-size: 22rpx; + color: #B0A090; + margin-bottom: 4rpx; + } + .reject-reason { + font-size: 24rpx; + color: #7A5A4A; + } + .detail-empty { + font-size: 24rpx; + color: #A0A0A0; + padding: 8rpx 0; + } + .info-grid { + display: flex; + flex-direction: column; + gap: 8rpx; + font-size: 24rpx; + color: #6B6B6B; + } + + /* 加载 */ + .loading-more, .no-more { + text-align: center; + padding: 30rpx; + font-size: 24rpx; + color: #A0A0A0; + } + .loading-mask { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(253, 251, 247, 0.8); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 999; + } + .loading-spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid #E0DDD5; + border-top-color: #C8A27A; + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin-bottom: 20rpx; + } + @keyframes spin { to { transform: rotate(360deg); } } \ No newline at end of file diff --git a/pages/kaohe_jinpai/kaohe_jinpai.js b/pages/kaohe_jinpai/kaohe_jinpai.js new file mode 100644 index 0000000..ed6fcb4 --- /dev/null +++ b/pages/kaohe_jinpai/kaohe_jinpai.js @@ -0,0 +1,461 @@ +// pages/kaohe_jinpai/kaohe_jinpai.js +const app = getApp(); +import request from '../../utils/request.js'; + +Page({ + data: { + activeTab: 'all_tags', + plateList: [], // 第一个接口返回的所有板块及标签(含 fee_list) + myTags: [], + recordList: [], + recordPage: 1, + recordHasMore: true, + recordLoading: false, + recordFilter: 'pending', + showApplyModal: false, + selectedTag: null, + applyGameNick: '', + applyRemark: '', + applyFee: 0, + applyJiluId: '', + isReapply: false, + payLoading: false, + ossImageUrl: app.globalData.ossImageUrl || '', + + // 新增:考核官相关 + kaoheguanList: [], // 所有可选的考核官 + showKaoheguanModal: false, // 考核官选择弹窗 + selectedKaoheguanId: '', // 已选择的考核官ID + selectedKaoheguanInfo: null, // 已选择的考核官信息(用于展示) + searchKeyword: '', // 搜索关键词 + filteredKaoheguanList: [], // 搜索过滤后的列表 + }, + + onLoad() { + this.fetchAllTags(); + this.fetchMyRecords(true); + }, + + switchTab(e) { + const tab = e.currentTarget.dataset.tab; + if (tab === this.data.activeTab) return; + this.setData({ activeTab: tab }); + if (tab === 'my_records') this.fetchMyRecords(true); + }, + + // 获取所有板块及标签(含费用列表),缓存到 plateList,同时获取考核官列表 + async fetchAllTags() { + try { + const res = await request({ url: '/dengji/dskhhq', method: 'POST' }); + if (res.data.code === 0) { + const { plate_list, my_tags, kaoheguan_list } = res.data.data; + // 拼接考核官头像为完整URL + const ossUrl = this.data.ossImageUrl; + const processedKgList = (kaoheguan_list || []).map(kg => ({ + ...kg, + avatar: kg.avatar && !kg.avatar.startsWith('http') ? ossUrl + kg.avatar : kg.avatar, + })); + this.setData({ + plateList: plate_list || [], + myTags: my_tags || [], + kaoheguanList: processedKgList // 使用拼接后的列表 + }); + } + } catch (e) {} + }, + + // 获取我的考核记录,并根据 plateList 计算本次/预计下次费用 + async fetchMyRecords(isRefresh = false) { + if (this.data.recordLoading) return; + const page = isRefresh ? 1 : this.data.recordPage; + this.setData({ recordLoading: true }); + try { + const res = await request({ + url: '/dengji/dskhjl', + method: 'POST', + data: { status: this.data.recordFilter, page, page_size: 5 } + }); + if (res.data.code === 0) { + const { list, has_more } = res.data.data; + + // 从 plateList 构建标签ID -> 完整标签对象的映射(用于精确匹配费用) + const tagMap = {}; + this.data.plateList.forEach(plate => { + (plate.tags || []).forEach(tag => { + tagMap[tag.id] = tag; // tag 包含 fee_list + }); + }); + + // 处理每条记录:优先使用 tag_obj.id 匹配,其次 chenghao_id + const processed = list.map(item => { + // 尝试多种方式获取标签ID + const tagId = (item.tag_obj && item.tag_obj.id) || item.chenghao_id; + const tag = tagMap[tagId] || item.tag_obj || {}; + const feeList = tag.fee_list || []; + let currentFee = 0; + let nextFee = 0; + if (feeList.length > 0) { + const cishu = item.cishu; + const cur = feeList.find(f => f.cishu === cishu); + currentFee = cur ? cur.feiyong : feeList[feeList.length - 1].feiyong; + const nextCishu = cishu + 1; + const nxt = feeList.find(f => f.cishu === nextCishu); + nextFee = nxt ? nxt.feiyong : feeList[feeList.length - 1].feiyong; + } + return { + ...item, + current_fee: currentFee, + next_fee: nextFee, + tag_obj: tag // 使用匹配到的完整标签对象,保证后续“再次考核”能拿到费用 + }; + }); + + const newList = isRefresh ? processed : [...this.data.recordList, ...processed]; + this.setData({ + recordList: newList, + recordPage: page + 1, + recordHasMore: has_more, + recordLoading: false + }); + } else { + this.setData({ recordLoading: false }); + } + } catch (e) { + this.setData({ recordLoading: false }); + } + }, + + filterRecord(e) { + const filter = e.currentTarget.dataset.filter; + if (filter === this.data.recordFilter) return; + this.setData({ recordFilter: filter, recordList: [], recordPage: 1, recordHasMore: true }); + this.fetchMyRecords(true); + }, + + onReachBottom() { + if (this.data.activeTab === 'my_records' && this.data.recordHasMore) { + this.fetchMyRecords(false); + } + }, + + // 打开申请考核弹窗,费用完全基于 plateList 中的标签数据 + openApplyModal(e) { + const { tag, jiluid, reapply } = e.currentTarget.dataset; + // 查找完整标签对象,保证 fee_list 存在 + let fullTag = tag; + if (fullTag && fullTag.fee_list === undefined) { + const found = this.findTagInPlateList(fullTag.id); + if (found) fullTag = found; + } + if (!fullTag) { + const found = this.findTagInPlateList(tag); + if (found) fullTag = found; + } + + let fee = 0; + if (fullTag) { + let nextCishu = 0; + // 再次申请时,基于原记录次数计算下一次考核次数 + if (reapply && jiluid) { + const record = this.data.recordList.find(r => r.jilu_id === jiluid); + if (record) { + nextCishu = record.cishu + 1; + } else { + nextCishu = (fullTag.my_cishu || 0) + 1; + } + } else { + nextCishu = (fullTag.my_cishu || 0) + 1; + } + const feeList = fullTag.fee_list || []; + const exact = feeList.find(f => f.cishu === nextCishu); + if (exact) { + fee = exact.feiyong; + } else if (feeList.length > 0) { + fee = feeList[feeList.length - 1].feiyong; + } + } + // 检查再次申请时原记录是否有考核官,若有则不允许指定 + let canSpecify = true; + let originalShenheguanId = ''; + if (reapply && jiluid) { + const record = this.data.recordList.find(r => r.jilu_id === jiluid); + if (record && record.shenheguan_id) { + canSpecify = false; // 已有考核官,不能指定新的 + originalShenheguanId = record.shenheguan_id; + } + } + this.setData({ + showApplyModal: true, + selectedTag: fullTag || null, + applyGameNick: '', + applyRemark: '', + applyFee: fee, + applyJiluId: jiluid || '', + isReapply: reapply || false, + selectedKaoheguanId: '', // 重置 + selectedKaoheguanInfo: null, + canSpecify: canSpecify, // 新增 + originalShenheguanId: originalShenheguanId, // 新增 + }); + }, + + // 辅助方法:从 plateList 查找指定 id 的标签 + findTagInPlateList(tagId) { + if (!tagId) return null; + for (const plate of this.data.plateList) { + for (const t of (plate.tags || [])) { + if (t.id === tagId) return t; + } + } + return null; + }, + + closeApplyModal() { + this.setData({ showApplyModal: false }); + }, + + // 实时更新游戏昵称 + onNickInput(e) { + this.setData({ applyGameNick: e.detail.value }); + }, + + // 实时更新备注 + onRemarkInput(e) { + this.setData({ applyRemark: e.detail.value }); + }, + + // 新增:打开考核官选择弹窗 + openKaoheguanModal() { + if (!this.data.canSpecify) { + wx.showToast({ title: '已有考核官,无法重新指定', icon: 'none' }); + return; + } + // 根据费用筛选可选考核官 + let all = [...this.data.kaoheguanList]; + if (this.data.applyFee > 0) { + all = all.filter(kg => kg.is_renzheng); + } + this.setData({ + showKaoheguanModal: true, + filteredKaoheguanList: all, + searchKeyword: '', + }); + }, + + // 关闭考核官选择弹窗 + closeKaoheguanModal() { + this.setData({ showKaoheguanModal: false }); + }, + + // 搜索输入 + onSearchInput(e) { + const value = e.detail.value.trim(); + this.setData({ searchKeyword: value }); + this.filterKaoheguanList(value); + }, + + // 过滤考核官列表 + filterKaoheguanList(keyword) { + let all = [...this.data.kaoheguanList]; + if (this.data.applyFee > 0) { + all = all.filter(kg => kg.is_renzheng); + } + if (keyword) { + all = all.filter(kg => + kg.yonghuid.includes(keyword) || + (kg.dashou_nick && kg.dashou_nick.includes(keyword)) + ); + } + this.setData({ filteredKaoheguanList: all }); + }, + + // 选择一个考核官 + selectKaoheguan(e) { + const kg = e.currentTarget.dataset.kg; + if (kg.shenhe_zhuangtai !== 0) { + wx.showToast({ title: '该考核官正忙,无法指定', icon: 'none' }); + return; + } + this.setData({ + selectedKaoheguanId: kg.yonghuid, + selectedKaoheguanInfo: kg, + showKaoheguanModal: false, + }); + }, + + // 取消已选择的考核官 + clearSelectedKaoheguan() { + this.setData({ + selectedKaoheguanId: '', + selectedKaoheguanInfo: null, + }); + }, + + // 提交申请 + async submitApply() { + const { applyGameNick, applyRemark, applyFee, selectedTag, isReapply, applyJiluId, selectedKaoheguanId } = this.data; + if (!applyGameNick || !applyGameNick.trim()) { + wx.showToast({ title: '请输入游戏昵称', icon: 'none' }); + return; + } + if (!applyRemark || !applyRemark.trim()) { + wx.showToast({ title: '请填写备注', icon: 'none' }); + return; + } + if (applyFee > 0) { + await this.startPayProcess(); + } else { + await this.doFreeApply(); + } + }, + + // 支付流程 + async startPayProcess() { + this.setData({ payLoading: true }); + try { + const res = await request({ + url: '/yonghu/khzf', + method: 'POST', + data: { + tag_id: this.data.selectedTag.id, + game_nick: this.data.applyGameNick, + remark: this.data.applyRemark, + reapply: this.data.isReapply, + jilu_id: this.data.applyJiluId, + shenheguan_id: this.data.selectedKaoheguanId, // 新增 + } + }); + if (res.data.code !== 200) throw new Error(res.data.message || '下单失败'); + const payParams = res.data.payParams; + const dingdanid = res.data.dingdanid; + wx.requestPayment({ + timeStamp: payParams.timeStamp, + nonceStr: payParams.nonceStr, + package: payParams.package, + signType: payParams.signType || 'MD5', + paySign: payParams.paySign, + success: async () => { + await this.pollPayStatus(dingdanid); + }, + fail: () => { + wx.showToast({ title: '支付取消或失败', icon: 'none' }); + request({ url: '/yonghu/khzfsb', method: 'POST', data: { dingdanid } }).catch(()=>{}); + } + }); + } catch (e) { + wx.showToast({ title: e.message || '支付异常', icon: 'none' }); + } finally { + this.setData({ payLoading: false }); + } + }, + + // 轮询支付结果 + async pollPayStatus(dingdanid) { + for (let i = 0; i < 10; i++) { + try { + const res = await request({ url: '/yonghu/khzffc', method: 'POST', data: { dingdanid } }); + if (res.data.code === 200) { + wx.showToast({ title: '申请成功', icon: 'success' }); + this.closeApplyModal(); + this.fetchAllTags(); + this.setData({ recordList: [], recordPage: 1 }); + this.fetchMyRecords(true); + return; + } + } catch (e) {} + await new Promise(r => setTimeout(r, 2000)); + } + wx.showToast({ title: '支付确认超时', icon: 'none' }); + }, + + // 免费申请 + async doFreeApply() { + try { + const res = await request({ + url: '/dengji/dssqkh', + method: 'POST', + data: { + tag_id: this.data.selectedTag.id, + game_nick: this.data.applyGameNick, + remark: this.data.applyRemark, + reapply: this.data.isReapply, + jilu_id: this.data.applyJiluId, + shenheguan_id: this.data.selectedKaoheguanId, // 新增 + } + }); + if (res.data.code === 0) { + wx.showToast({ title: '申请成功', icon: 'success' }); + this.closeApplyModal(); + this.fetchAllTags(); + this.setData({ recordList: [], recordPage: 1 }); + this.fetchMyRecords(true); + } else { + wx.showToast({ title: res.data.msg || '申请失败', icon: 'none' }); + } + } catch (e) { + wx.showToast({ title: '网络错误', icon: 'none' }); + } + }, + + // 联系考核官(私聊) + contactKaoheguan(e) { + const item = e.currentTarget.dataset.item; + if (!item) return; + if (!item.shenheguan_id) { + wx.showToast({ title: '暂无考核官信息', icon: 'none' }); + return; + } + const uid = wx.getStorageSync('uid'); + if (!uid) { + wx.showToast({ title: '用户信息缺失', icon: 'none' }); + return; + } + if (app.globalData.currentRole !== 'dashou') { + app.globalData.currentRole = 'dashou'; + wx.setStorageSync('currentRole', 'dashou'); + } + if (app.connectForCurrentRole) { + app.connectForCurrentRole(); + } else if (app.ensureConnection) { + app.ensureConnection(); + } + const ossImageUrl = this.data.ossImageUrl || app.globalData.ossImageUrl || ''; + let fullAvatar = item.shenheguan_avatar || ''; + if (fullAvatar && !fullAvatar.startsWith('http')) { + fullAvatar = ossImageUrl + fullAvatar; + } + if (!fullAvatar) { + fullAvatar = ossImageUrl + (app.globalData.morentouxiang || 'avatar/default.jpg'); + } + const param = { + toUserId: 'Kh' + item.shenheguan_id, + toName: '考核官' + item.shenheguan_id, + toAvatar: fullAvatar + }; + wx.navigateTo({ + url: '/pages/liaotian/liaotian?data=' + encodeURIComponent(JSON.stringify(param)), + fail: (err) => { + wx.showToast({ title: '打开聊天失败', icon: 'none' }); + } + }); + }, + + showTagDetail(e) { + const tag = e.currentTarget.dataset.tag; + const feeStr = this.formatFeeList(tag.fee_list); + wx.showModal({ + title: tag.name, + content: `规则:${tag.rule || '暂无'}\n${tag.jinpai ? '【可同时开启金牌】' : ''}\n费用:${feeStr}`, + showCancel: false, + confirmText: '知道了' + }); + }, + + formatFeeList(feeList) { + if (!feeList || feeList.length === 0) return '暂无'; + let str = feeList.map(f => `第${f.cishu}次:¥${f.feiyong}`).join(';'); + const last = feeList[feeList.length - 1]; + str += `;第${last.cishu}次及以上:¥${last.feiyong}`; + return str; + }, +}); \ No newline at end of file diff --git a/pages/kaohe_jinpai/kaohe_jinpai.json b/pages/kaohe_jinpai/kaohe_jinpai.json new file mode 100644 index 0000000..fa331e5 --- /dev/null +++ b/pages/kaohe_jinpai/kaohe_jinpai.json @@ -0,0 +1,9 @@ +{ + "usingComponents": { + "chenghao-tag": "/components/chenghao-tag/chenghao-tag", + "global-notification": "/components/global-notification/global-notification" + }, + "navigationBarTitleText": "考核金牌", + "navigationBarBackgroundColor": "#0F0A1E", + "navigationBarTextStyle": "white" + } \ No newline at end of file diff --git a/pages/kaohe_jinpai/kaohe_jinpai.wxml b/pages/kaohe_jinpai/kaohe_jinpai.wxml new file mode 100644 index 0000000..6d89494 --- /dev/null +++ b/pages/kaohe_jinpai/kaohe_jinpai.wxml @@ -0,0 +1,135 @@ + + + + + 考核标签 + 我的考核 + + + + + + + {{item.mingcheng}} + + + + 金牌 + + + {{item.cishu}}次 ¥{{item.feiyong}} + ≥{{item.fee_list.length}}次 ¥{{item.fee_list[item.fee_list.length-1].feiyong}} + + 申请考核 + 已拥有 + + + + + + + + + 进行中/未通过 + 已通过 + + + + + {{item.chenghao_name}} + {{item.statusText}} + + + 板块:{{item.bankuai_name}} + 考核官:{{item.shenheguan_id || '待分配'}} + + 联系考核官 + + 游戏ID:{{item.dashou_youxi_id}} + 备注:{{item.dashou_beizhu}} + 第{{item.cishu}}次考核 · 共缴 ¥{{item.total_fee}} + + + + 本次考核费用:¥{{item.current_fee}} + 预计下次费用:¥{{item.next_fee}} + + + 上次拒绝原因:{{item.last_fail_reason}} + + + 再次考核 + + 加载中... + —— 没有更多了 —— + + + + + + + 申请考核 - {{selectedTag.name}} + + 游戏昵称 + + + + 备注 +