diff --git a/a_long_dianjing/celery.py b/a_long_dianjing/celery.py index e4d8c8a..d25e27d 100644 --- a/a_long_dianjing/celery.py +++ b/a_long_dianjing/celery.py @@ -73,12 +73,12 @@ app.conf.beat_schedule = { # 🔥 核心任务:订单调度器(每2分钟运行) - # 🔥 补偿检查任务(每5分钟运行) - 'check_order_expire_task': { - 'task': 'dingdan.tasks.check_order_expire_task', - 'schedule': crontab(minute='*/5'), - 'options': {'queue': 'order_tasks', 'priority': 3}, - }, + # 🔥 补偿检查任务(每5分钟运行)— 已禁用:见 settings.ORDER_AUTO_SETTLE_ENABLED + # 'check_order_expire_task': { + # 'task': 'dingdan.tasks.check_order_expire_task', + # 'schedule': crontab(minute='*/5'), + # 'options': {'queue': 'order_tasks', 'priority': 3}, + # }, # 1. 每日凌晨0点执行 - 清零任务 diff --git a/a_long_dianjing/settings.py b/a_long_dianjing/settings.py index 3f06694..ed75a5d 100644 --- a/a_long_dianjing/settings.py +++ b/a_long_dianjing/settings.py @@ -459,7 +459,10 @@ CELERY_TASK_QUEUES = { # 所有时间配置都在这里,方便修改 # 订单超时时间(秒) - 48小时 -ORDER_EXPIRE_SECONDS = 48000 * 60 * 60 # 172800秒 +ORDER_EXPIRE_SECONDS = 48 * 60 * 60 # 172800秒 + +# 订单自动结算(Celery 延时任务 + 定时补偿)是否启用 +ORDER_AUTO_SETTLE_ENABLED = False # 每日任务执行时间(24小时制) DAILY_TASK_HOUR = 0 # 0点 @@ -532,6 +535,61 @@ BATCH_LINK_DEFAULT_COUNT = 1 BATCH_LINK_MAX_COUNT = 100 +# ==================== 日志配置(终端输出,便于生产排查) ==================== +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '[{asctime}] {levelname} {name} {pathname}:{lineno} — {message}', + 'style': '{', + }, + 'simple': { + 'format': '[{asctime}] {levelname} {name} — {message}', + 'style': '{', + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + 'level': 'INFO', + }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': False, + }, + 'django.request': { + 'handlers': ['console'], + 'level': 'ERROR', + 'propagate': False, + }, + 'yonghu': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': False, + }, + 'dingdan': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': False, + }, + 'utils': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': False, + }, + }, +} + + diff --git a/dingdan/signals.py b/dingdan/signals.py index c14aa5f..c71e2a7 100644 --- a/dingdan/signals.py +++ b/dingdan/signals.py @@ -76,6 +76,10 @@ def handle_order_status_8(sender, instance, **kwargs): # 状态从 非8 变为 8 if old != 8 and instance.zhuangtai == 8: + if not getattr(settings, 'ORDER_AUTO_SETTLE_ENABLED', False): + logger.info(f"订单 {instance.dingdan_id} 状态变为8,自动结算已禁用,不提交任务") + return + logger.info(f"📅 订单 {instance.dingdan_id} 状态变为8,提交定时任务") # 设置时间标记 diff --git a/dingdan/tasks.py b/dingdan/tasks.py index 2cd56ec..564c36c 100644 --- a/dingdan/tasks.py +++ b/dingdan/tasks.py @@ -33,8 +33,10 @@ def process_expired_order(self, dingdan_id): """ 处理单个超时订单(订单状态=8,超过48小时未处理) """ - - + if not getattr(settings, 'ORDER_AUTO_SETTLE_ENABLED', False): + logger.info('订单自动结算已禁用,跳过 process_expired_order(%s)', dingdan_id) + return f'自动结算已禁用,跳过订单{dingdan_id}' + try: # 使用select_for_update锁定记录,防止并发修改 with transaction.atomic(): @@ -149,6 +151,10 @@ def check_order_expire_task(): 每5分钟执行一次,作为信号处理的备用方案 查找状态为8且创建时间超过72小时的订单(宽松时间,防止误处理) """ + if not getattr(settings, 'ORDER_AUTO_SETTLE_ENABLED', False): + logger.info('订单自动结算已禁用,跳过 check_order_expire_task') + return {"success": True, "message": "自动结算已禁用", "count": 0} + try: # 计算更宽松的时间点:当前时间减去72小时(48+24小时) # 这样避免误处理刚变为状态8的订单 diff --git a/utils/exception_handler.py b/utils/exception_handler.py index 55f4576..590e1c2 100644 --- a/utils/exception_handler.py +++ b/utils/exception_handler.py @@ -1,32 +1,33 @@ -# /opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.443/django/utils/exception_handler.py - from rest_framework.views import exception_handler from rest_framework.response import Response from rest_framework import status import logging -import os +import traceback + +from django.conf import settings + +logger = logging.getLogger('django.request') -logger = logging.getLogger('django') def custom_exception_handler(exc, context): """ - 生产环境安全异常处理器 - 不泄露任何敏感信息 - 基于你之前项目的代码,做了小优化 + 统一异常处理:所有错误写入终端日志(logger),生产环境响应不泄露细节。 """ - # 先调用默认异常处理器 response = exception_handler(exc, context) - + request = context.get('request') + path = getattr(request, 'path', 'unknown') + method = getattr(request, 'method', 'unknown') + user = request.user if request else None + user_info = f"用户: {user.id if user and user.is_authenticated else '未登录'}" + if response is not None: - # 🔥 记录原始错误信息到日志(便于排查) - request = context['request'] - user_info = f"用户: {request.user.id if request.user.is_authenticated else '未登录'}" logger.error( - f"API异常 - 路径: {request.path}, " - f"方法: {request.method}, {user_info}, " - f"状态码: {response.status_code}, 错误: {str(exc)}" + 'API异常 path=%s method=%s %s status=%s error=%s', + path, method, user_info, response.status_code, exc, + exc_info=exc, ) - - # 🔥 生产环境返回标准化错误,不暴露任何细节 + if settings.DEBUG: + return response error_messages = { status.HTTP_400_BAD_REQUEST: {'code': 400, 'message': '请求参数错误'}, status.HTTP_401_UNAUTHORIZED: {'code': 401, 'message': '请先登录'}, @@ -40,19 +41,27 @@ def custom_exception_handler(exc, context): status.HTTP_503_SERVICE_UNAVAILABLE: {'code': 503, 'message': '服务暂不可用'}, status.HTTP_504_GATEWAY_TIMEOUT: {'code': 504, 'message': '网关超时'}, } - - # 🔥 如果是调试模式,返回详细错误(便于开发) - from django.conf import settings - if settings.DEBUG: - return response - - # 🔥 生产环境返回标准错误 standard_response = error_messages.get( - response.status_code, - {'code': response.status_code, 'message': '请求错误'} + response.status_code, + {'code': response.status_code, 'message': '请求错误'}, ) - return Response(standard_response, status=response.status_code) - - # 对于非DRF异常,这里可以根据需要处理 - return response \ No newline at end of file + + # DRF 未处理的异常(数据库字段缺失、AttributeError 等)→ 500 且无日志是常见问题 + logger.error( + '未处理API异常 path=%s method=%s %s error=%s traceback=%s', + path, + method, + user_info, + exc, + traceback.format_exc(), + ) + if settings.DEBUG: + return Response( + {'code': 500, 'message': str(exc), 'detail': traceback.format_exc()}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + return Response( + {'code': 500, 'message': '服务器内部错误'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) diff --git a/yonghu/views.py b/yonghu/views.py index 8cf1480..bc4cff3 100644 --- a/yonghu/views.py +++ b/yonghu/views.py @@ -8672,6 +8672,21 @@ class KefuGetOrderDetailView(APIView): def post(self, request): start_time = time.time() + try: + return self._build_order_detail_response(request, start_time) + except Exception as e: + logger.exception( + '客服订单详情异常 dingdan_id=%s phone=%s error=%s', + request.data.get('dingdan_id'), + request.data.get('phone'), + e, + ) + return Response( + {'code': 500, 'msg': '获取订单详情失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + def _build_order_detail_response(self, request, start_time): # 1. 获取参数 phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() @@ -8716,16 +8731,9 @@ class KefuGetOrderDetailView(APIView): # 3. 查询订单 try: - # 使用 select_related 预加载扩展表,避免后续 N+1 查询 dingdan_obj = Dingdan.objects.select_related( 'pingtai_kuozhan', 'shangjia_kuozhan' - ).only( - 'dingdan_id', 'zhuangtai', 'fadan_pingtai', 'jine', 'dashou_fencheng', - 'guanshi_fencheng', 'guanshi_shangjia_fencheng', - 'jiedan_dashou_id', 'dashou_liuyan', 'zhiding_id', 'shangpin_id', - 'leixing_id', 'tupian', 'jieshao', 'beizhu', 'tkly', 'nicheng', - 'clkf', 'create_time', 'update_time' ).get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: @@ -8735,15 +8743,21 @@ class KefuGetOrderDetailView(APIView): status=status.HTTP_404_NOT_FOUND ) + def _to_float(val, default=0.0): + try: + return float(val) if val is not None else default + except (TypeError, ValueError): + return default + # 4. 构建基础响应数据(主表字段) response_data = { 'dingdan_id': dingdan_obj.dingdan_id or '', 'zhuangtai': dingdan_obj.zhuangtai or 0, 'fadanpingtai': dingdan_obj.fadan_pingtai or 0, - 'jiage': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00, - 'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00, - 'guanshi_fencheng': float(dingdan_obj.guanshi_fencheng) if dingdan_obj.guanshi_fencheng else 0.00, - 'guanshi_shangjia_fencheng': float(dingdan_obj.guanshi_shangjia_fencheng) if dingdan_obj.guanshi_shangjia_fencheng else 0.00, + 'jiage': _to_float(dingdan_obj.jine), + 'dashou_fencheng': _to_float(dingdan_obj.dashou_fencheng), + 'guanshi_fencheng': _to_float(getattr(dingdan_obj, 'guanshi_fencheng', 0)), + 'guanshi_shangjia_fencheng': _to_float(getattr(dingdan_obj, 'guanshi_shangjia_fencheng', 0)), 'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '', 'dashou_liuyan': dingdan_obj.dashou_liuyan or '', 'zhiding_id': dingdan_obj.zhiding_id or '',