第一次提交:小程序前端最新版本

This commit is contained in:
XingQue
2026-06-14 02:38:05 +08:00
commit 5e90f7c87c
677 changed files with 153758 additions and 0 deletions

31
.eslintrc.js Normal file
View File

@@ -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: {},
}

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
miniprogram_npm/
project.private.config.json
.DS_Store
*.log

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

30
.idea/a_long_dianjing.iml generated Normal file
View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="a_long_dianjing/settings.py" />
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (a_long_dianjing)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/../a_long_dianjing\templates" />
</list>
</option>
</component>
</module>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13 (a_long_dianjing)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (a_long_dianjing)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/a_long_dianjing.iml" filepath="$PROJECT_DIR$/.idea/a_long_dianjing.iml" />
</modules>
</component>
</project>

3
.vs/ProjectSettings.json Normal file
View File

@@ -0,0 +1,3 @@
{
"CurrentProjectSetting": null
}

View File

@@ -0,0 +1,7 @@
{
"ExpandedNodes": [
""
],
"SelectedNode": "\\app.js",
"PreviewInSolutionExplorer": false
}

Binary file not shown.

View File

@@ -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": ""
}
]
}
]
}
]
}

BIN
.vs/slnx.sqlite Normal file

Binary file not shown.

21
LICENSE Normal file
View File

@@ -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.

2
_init_.py Normal file
View File

@@ -0,0 +1,2 @@
# 3. 在项目的 `__init__.py` 文件与manage.py同级添加两行代码
# 文件路径a_long_dianjing/__init__.py

View File

@@ -0,0 +1,8 @@
"""
阿龙电竞 - Django项目初始化
确保Celery在Django启动时自动加载
"""
from .celery import app as celery_app
__all__ = ('celery_app',)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
a_long_dianjing/asgi.py Normal file
View File

@@ -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()

115
a_long_dianjing/celery.py Normal file
View File

@@ -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小时

View File

@@ -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'
))

461
a_long_dianjing/settings.py Normal file
View File

@@ -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 = '' # 你的腾讯云APPIDjulebu-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' # 你的腾讯云APPIDjulebu-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'
# 提现收款链路日志(输出到 stderrgunicorn/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,
},
},
}

60
a_long_dianjing/urls.py Normal file
View File

@@ -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路由转发

16
a_long_dianjing/wsgi.py Normal file
View File

@@ -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()

325
app.js Normal file
View File

@@ -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);
}
}
}
});

107
app.json Normal file
View File

@@ -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"
}

10
app.wxss Normal file
View File

@@ -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;
}

View File

@@ -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);' });
}
}
}
});

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,20 @@
<!-- components/chenghao-tag/chenghao-tag.wxml -->
<view
class="tag-root {{shapeClass}} {{animationClass}}"
style="width: {{width}}rpx; height: {{height}}rpx; {{bgStyle}}"
>
<!-- 背景图(如果有) -->
<image
wx:if="{{imageUrl}}"
class="tag-bg-image"
src="{{imageUrl}}"
mode="aspectFill"
binderror="onImageError"
></image>
<!-- 文字层 -->
<text
class="tag-text"
style="color: {{textColor}}; font-size: {{textSize}}rpx;"
>{{mingcheng}}</text>
</view>

View File

@@ -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); }
}

View File

@@ -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' });
}
}
}
});

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,49 @@
<view
class="global-notification {{show ? 'show' : ''}} {{positionClass}} {{swipingClass}}"
bindtap="onTap"
bindtouchstart="onTouchStart"
bindtouchmove="onTouchMove"
bindtouchend="onTouchEnd"
style="{{swipeStyle}}"
data-notification-id="{{notificationData && notificationData.id ? notificationData.id : ''}}">
<!-- 滑动关闭指示器 -->
<view class="notification-swipe-indicator {{isSwiping ? 'active' : ''}}">
<view class="swipe-line"></view>
</view>
<!-- 通知内容 -->
<view class="notification-content">
<!-- 左侧头像 -->
<image class="notification-avatar" src="{{avatar}}" mode="aspectFill"></image>
<!-- 中间内容区域 -->
<view class="notification-body">
<view class="notification-title">{{title}}</view>
<view class="notification-message">{{message}}</view>
<view class="notification-time" wx:if="{{showTime}}">
{{formatTime(notificationData ? notificationData.timestamp : '')}}
</view>
</view>
<!-- 右侧操作区域 -->
<view class="notification-actions">
<!-- 关闭按钮 -->
<view class="notification-close" catchtap="onClose">
<text class="close-icon">×</text>
</view>
<!-- 免打扰按钮 -->
<view class="notification-mute {{isMuted ? 'muted' : ''}}" catchtap="onMute" wx:if="{{showMuteButton}}">
<text class="mute-icon">
{{isMuted ? '🔕' : '🔔'}}
</text>
</view>
</view>
</view>
<!-- 进度条 -->
<view class="notification-progress" wx:if="{{showProgress}}">
<view class="progress-bar {{progress < 30 ? 'warning' : ''}}"></view>
</view>
</view>

View File

@@ -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;
}

View File

@@ -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 }, {})
}
},
})

View File

@@ -0,0 +1,5 @@
{
"component": true,
"styleIsolation": "apply-shared",
"usingComponents": {}
}

View File

@@ -0,0 +1,64 @@
<view class="weui-navigation-bar {{extClass}}">
<view class="weui-navigation-bar__inner {{ios ? 'ios' : 'android'}}" style="color: {{color}}; background: {{background}}; {{displayStyle}}; {{innerPaddingRight}}; {{safeAreaTop}}">
<!-- 左侧按钮 -->
<view class='weui-navigation-bar__left' style="{{leftWidth}}">
<block wx:if="{{back || homeButton}}">
<!-- 返回上一页 -->
<block wx:if="{{back}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_goback">
<view
bindtap="back"
class="weui-navigation-bar__btn_goback_wrapper"
hover-class="weui-active"
hover-stay-time="100"
aria-role="button"
aria-label="返回"
>
<view class="weui-navigation-bar__button weui-navigation-bar__btn_goback"></view>
</view>
</view>
</block>
<!-- 返回首页 -->
<block wx:if="{{homeButton}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_home">
<view
bindtap="home"
class="weui-navigation-bar__btn_home_wrapper"
hover-class="weui-active"
aria-role="button"
aria-label="首页"
>
<view class="weui-navigation-bar__button weui-navigation-bar__btn_home"></view>
</view>
</view>
</block>
</block>
<block wx:else>
<slot name="left"></slot>
</block>
</view>
<!-- 标题 -->
<view class='weui-navigation-bar__center'>
<view wx:if="{{loading}}" class="weui-navigation-bar__loading" aria-role="alert">
<view
class="weui-loading"
aria-role="img"
aria-label="加载中"
></view>
</view>
<block wx:if="{{title}}">
<text>{{title}}</text>
</block>
<block wx:else>
<slot name="center"></slot>
</block>
</view>
<!-- 右侧留空 -->
<view class='weui-navigation-bar__right'>
<slot name="right"></slot>
</view>
</view>
</view>

View File

@@ -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);
}
}

View File

@@ -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);
}
});
});
}
}
});

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,20 @@
<view class="order-card" wx:if="{{order}}">
<view class="card-header">
<text class="card-title">关联订单</text>
<text class="card-id">{{order.dingdan_id}}</text>
<!-- 跨平台标记 -->
<text class="cross-tag" wx:if="{{isCross}}">跨平台</text>
</view>
<view class="card-body" bindtap="sendOrderMessage">
<image class="card-img" src="{{order.tupian || '/images/default-goods.png'}}" mode="aspectFill" />
<view class="card-info">
<text class="card-jieshao">{{order.jieshao || '暂无介绍'}}</text>
<text class="card-jine">¥{{order.jine}}</text>
</view>
<view class="card-send-btn">发送</view>
</view>
<view wx:if="{{errorMsg}}" class="card-error">{{errorMsg}}</view>
</view>
<view class="order-card" wx:elif="{{loading}}">
<text class="loading-text">加载订单信息...</text>
</view>

View File

@@ -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; }

View File

@@ -0,0 +1,16 @@
const app = getApp();
Component({
properties: {
visible: Boolean,
showDetail: { type: Boolean, value: false }
},
data: {},
methods: {
close() {
this.triggerEvent('close');
}
}
});

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,12 @@
<view class="order-sender-mask" wx:if="{{visible}}" catchtap="close"></view>
<view class="order-sender {{visible ? 'show' : ''}}">
<view class="header">
<text class="title">提示</text>
<text class="close" bindtap="close">✕</text>
</view>
<view class="content">
<view class="icon">📋</view>
<text class="notice">此功能暂未开放</text>
<text class="sub">敬请期待</text>
</view>
</view>

View File

@@ -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;
}

View File

@@ -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() {}
}
});

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,110 @@
<!-- components/popup-notice/popup-notice.wxml -->
<!-- 全屏弹窗遮罩层(完全不变) -->
<view class="popup-mask" wx:if="{{visible}}" catchtouchmove="preventMove">
<view class="popup-container {{isFullscreen ? 'fullscreen' : ''}}">
<!-- 右上角操作按钮组 -->
<view class="action-buttons">
<!-- 最小化按钮 -->
<view class="minimize-btn" bindtap="onMinimize">
<text class="btn-icon"></text>
</view>
<!-- 全屏切换按钮 -->
<view class="fullscreen-btn" bindtap="toggleFullscreen">
<text class="btn-icon">{{isFullscreen ? '✕' : '⛶'}}</text>
</view>
</view>
<!-- 标题 -->
<view wx:if="{{title}}" class="popup-title">{{title}}</view>
<!-- 可滚动内容区 -->
<view class="scroll-wrapper">
<scroll-view scroll-y class="scroll-view" enhanced show-scrollbar="{{true}}">
<view wx:if="{{hasTextOnly}}" class="text-content">{{textContent}}</view>
<view wx:else>
<view wx:for="{{processedImages}}" wx:key="index" class="image-block">
<view wx:if="{{item.text}}" class="block-text">{{item.text}}</view>
<view class="image-padding">
<image
class="block-image"
src="{{item.url}}"
style="width:100%; height:{{item.height}}px; display:block;"
data-index="{{index}}"
data-url="{{item.url}}"
bindtap="onCustomPreview"
binderror="onImageError"
/>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 底部固定区 -->
<view class="bottom-fixed">
<view wx:if="{{remainingSeconds > 0}}" class="countdown-tip">
请阅读 {{remainingSeconds}} 秒后关闭
</view>
<view wx:if="{{remainingSeconds === 0 && showMuteCheckbox}}" class="mute-row">
<checkbox-group bindchange="onMuteChange">
<checkbox value="mute" color="#000" />
<text class="mute-label">今日不再提示</text>
</checkbox-group>
</view>
<!-- 关闭按钮 -->
<view
class="close-btn {{remainingSeconds > 0 ? 'close-btn-disabled' : ''}}"
bindtap="{{remainingSeconds > 0 ? '' : 'onCloseTap'}}"
>
<text class="close-btn-text">{{remainingSeconds > 0 ? '阅读中...' : '关闭'}}</text>
<view class="close-btn-sweep" wx:if="{{remainingSeconds === 0}}"></view>
</view>
</view>
</view>
</view>
<!-- 悬浮窗(自定义触摸拖动) -->
<view
class="floating-avatar-wrapper"
wx:if="{{isMinimized}}"
style="left: {{floatX}}px; top: {{floatY}}px;"
catchtouchstart="onFloatTouchStart"
catchtouchmove="onFloatTouchMove"
catchtouchend="onFloatTouchEnd"
>
<image
class="floating-avatar"
src="{{floatAvatarUrl}}"
mode="aspectFill"
binderror="onFloatAvatarError"
/>
</view>
<!-- 🆕 自定义图片预览遮罩(全屏,不触发页面生命周期) -->
<view
class="custom-preview-mask"
wx:if="{{showCustomPreview}}"
catchtouchmove="preventMove"
bindtap="onCloseCustomPreview"
>
<view class="preview-header">
<text class="preview-index">{{currentPreviewIndex + 1}} / {{previewUrls.length}}</text>
<view class="preview-close" bindtap="onCloseCustomPreview">×</view>
</view>
<swiper
class="preview-swiper"
current="{{currentPreviewIndex}}"
bindchange="onPreviewSwiperChange"
indicator-dots="{{false}}"
>
<swiper-item wx:for="{{previewUrls}}" wx:key="index">
<image
src="{{item}}"
mode="aspectFit"
class="preview-image"
catchtap="stopPropagation"
/>
</swiper-item>
</swiper>
</view>

View File

@@ -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%;
}

144
config/roles.js Normal file
View File

@@ -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 };

194
custom-tab-bar/index.js Normal file
View File

@@ -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 });
}
}
});

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

42
custom-tab-bar/index.wxml Normal file
View File

@@ -0,0 +1,42 @@
<view class="custom-tab-bar">
<view class="bar-row">
<!-- 左侧椭圆身份按钮 -->
<view class="role-switch-btn" catchtap="toggleRolePicker">
<image class="role-switch-icon" src="/images/role-switch.png" mode="aspectFit" />
<text class="role-switch-text">切换</text>
</view>
<!-- 右侧纯黑闪光胶囊 -->
<view class="tab-capsule">
<block wx:for="{{tabList}}" wx:key="index">
<view class="tab-item {{selectedIndex === index ? 'active' : ''}}"
data-path="{{item.pagePath}}"
data-index="{{index}}"
catchtap="switchTab">
<!-- 🔥 根据页面路径判断是否显示角标,不再写死 index -->
<view class="badge-container" wx:if="{{item.pagePath === 'pages/xiaoxi/xiaoxi' && badgeText}}">
<view class="tab-badge">{{badgeText}}</view>
</view>
<image class="tab-icon" src="{{selectedIndex === index ? item.selectedIconPath : item.iconPath}}" mode="aspectFit" />
<text class="tab-text">{{item.text}}</text>
</view>
</block>
</view>
</view>
<!-- 身份选择弹窗 -->
<view wx:if="{{showRolePicker}}" class="picker-mask" catchtap="toggleRolePicker"></view>
<view wx:if="{{showRolePicker}}" class="picker-panel">
<view class="picker-header">
<text class="picker-title">切换身份</text>
<text class="picker-close" catchtap="toggleRolePicker">✕</text>
</view>
<block wx:for="{{availableRoles}}" wx:key="key">
<view class="role-option {{currentRole === item.key ? 'active' : ''}}"
data-role="{{item.key}}" catchtap="selectRole">
<text>{{item.name}}</text>
<text wx:if="{{currentRole === item.key}}" class="check-mark">✓</text>
</view>
</block>
</view>
</view>

165
custom-tab-bar/index.wxss Normal file
View File

@@ -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;
}

0
dengji/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

3
dengji/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
dengji/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class DengjiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "dengji"

View File

235
dengji/models.py Normal file
View File

@@ -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

87
dengji/services.py Normal file
View File

@@ -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)

3
dengji/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

22
dengji/urls.py Normal file
View File

@@ -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='考核官获取考核记录'),
]

403
dengji/utils.py Normal file
View File

@@ -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

975
dengji/views.py Normal file
View File

@@ -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)

0
dingdan/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
dingdan/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

10
dingdan/apps.py Normal file
View File

@@ -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

View File

@@ -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)

View File

@@ -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",
},
),
]

View File

@@ -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="佣金要求",
),
),
]

View File

@@ -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",
},
),
]

View File

@@ -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="游戏昵称"
),
),
]

View File

@@ -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"
),
),
]

View File

@@ -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"
)
],
},
),
]

View File

@@ -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"
),
),
]

View File

@@ -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驳回",
),
),
]

View File

@@ -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="聊天频道名"
),
),
]

View File

@@ -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"
),
],
},
),
]

View File

@@ -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="商家昵称"),
),
]

View File

@@ -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的时间"
),
),
]

Some files were not shown because too many files have changed in this diff Show More