"""Settlement API views""" import logging import re from datetime import datetime, date from django.contrib.auth import get_user_model from django.utils import timezone from rest_framework import status, authentication from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from rest_framework_simplejwt.authentication import JWTAuthentication from basic_info.models import Merchant from settlement.services import ( DEFAULT_DESIGNER_SUMMARY_STATE_NAMES, DEFAULT_DESIGNER_TASK_STATE_NAME_SUFFIX, DEFAULT_DESIGNER_PARAM_KEY, DEFAULT_DESIGNER_TASK_QUANTITY_PARAM_KEY, DEFAULT_DESIGNER_TASK_TIME_PARAM_KEY, get_plate_order_summary_by_customer, get_plate_order_summary_by_designer, get_designer_workflow_task_summary, ) from .mixins import SettlementVisibilityMixin logger = logging.getLogger(__name__) AGENT_SECRET = "RCYH_BOT_0083" class AgentSecretAuthentication(authentication.BaseAuthentication): """ Agent 密钥认证(仅用于 settlement/plate-orders/summary API) 当请求头中存在 X-AGENT-SECRET 且值匹配时,跳过 JWT 认证, 以 admin 超级管理员身份执行请求。 """ def authenticate(self, request): secret = request.META.get("HTTP_X_AGENT_SECRET") if secret != AGENT_SECRET: return None User = get_user_model() try: user = User.objects.get(username="admin", is_superuser=True) return (user, None) except User.DoesNotExist: logger.warning("[AgentSecretAuthentication] admin 用户不存在") return None class PlateOrderSummaryView(SettlementVisibilityMixin, APIView): """ 开版订单统计 API GET /api/v1/settlement/plate-orders/summary/ 参数: date: 统计日期(YYYY-MM-DD) 认证方式: - JWT Token(标准方式) - X-AGENT-SECRET: RCYH_BOT_0083(Agent 专用,跳过鉴权) 可见性规则(与 plate-order 列表接口对齐): - superuser 或持有 view_all_permission(默认 printing.view_all_plateorders) → 返回该商户下全部客户的开版汇总 - 普通员工 → 仅返回其负责/可见客户的开版汇总 """ authentication_classes = [ AgentSecretAuthentication, authentication.SessionAuthentication, JWTAuthentication, ] permission_classes = [IsAuthenticated] view_all_permission = "printing.view_all_plateorders" def get(self, request): """ 获取开版订单统计 """ date_str = request.query_params.get("date") if not date_str: return Response( {"error": "缺少 date 参数"}, status=status.HTTP_400_BAD_REQUEST ) if not re.match(r"^\d{4}-\d{2}-\d{2}$", date_str): return Response( {"error": "日期格式错误,请使用 YYYY-MM-DD 格式"}, status=status.HTTP_400_BAD_REQUEST, ) try: settlement_date = date.fromisoformat(date_str) except ValueError: return Response({"error": "日期不存在"}, status=status.HTTP_403_FORBIDDEN) emp = getattr(request.user, "employee", None) if emp is None or emp.merchant is None: if request.user.is_superuser: merchant = Merchant.objects.first() if merchant is None: return Response( {"error": "系统中没有商户"}, status=status.HTTP_403_FORBIDDEN ) merchant_id = merchant.id else: return Response( {"error": "用户未关联商户"}, status=status.HTTP_403_FORBIDDEN ) else: merchant_id = emp.merchant.id try: data = get_plate_order_summary_by_customer( merchant_id=merchant_id, settlement_date=settlement_date, user=self.get_service_user(request.user), ) return Response({"data": data}) except Exception as e: logger.exception(f"[settlement.views] 获取开版订单统计失败: {e}") return Response( {"error": "获取统计数据失败"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) class PlateOrderDesignerSummaryView(SettlementVisibilityMixin, APIView): """ 开版订单设计师统计 API GET /api/v1/settlement/plate-orders/designer-summary/ 参数: date: 统计日期(YYYY-MM-DD) state_names: 可选,逗号分隔或重复传参的 state 名称,默认“画图完成” """ authentication_classes = PlateOrderSummaryView.authentication_classes permission_classes = [IsAuthenticated] view_all_permission = "printing.view_all_plateorders" def get(self, request): date_str = request.query_params.get("date") if not date_str: return Response( {"error": "缺少 date 参数"}, status=status.HTTP_400_BAD_REQUEST ) if not re.match(r"^\d{4}-\d{2}-\d{2}$", date_str): return Response( {"error": "日期格式错误,请使用 YYYY-MM-DD 格式"}, status=status.HTTP_400_BAD_REQUEST, ) try: settlement_date = date.fromisoformat(date_str) except ValueError: return Response({"error": "日期不存在"}, status=status.HTTP_403_FORBIDDEN) emp = getattr(request.user, "employee", None) if emp is None or emp.merchant is None: if request.user.is_superuser: merchant = Merchant.objects.first() if merchant is None: return Response( {"error": "系统中没有商户"}, status=status.HTTP_403_FORBIDDEN ) merchant_id = merchant.id else: return Response( {"error": "用户未关联商户"}, status=status.HTTP_403_FORBIDDEN ) else: merchant_id = emp.merchant.id state_names = _parse_state_names(request) try: data = get_plate_order_summary_by_designer( merchant_id=merchant_id, settlement_date=settlement_date, user=self.get_service_user(request.user), state_names=state_names, ) return Response({ "data": data, "meta": { "state_names": list(state_names or DEFAULT_DESIGNER_SUMMARY_STATE_NAMES), "designer_param_key": DEFAULT_DESIGNER_PARAM_KEY, }, }) except ValueError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.exception(f"[settlement.views] 获取开版订单设计师统计失败: {e}") return Response( {"error": "获取统计数据失败"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) def _parse_state_names(request): """解析 state_names,支持逗号分隔和重复传参。""" return _parse_csv_query_param(request, "state_names") def _parse_designer_names(request): """解析 designer_names,支持逗号分隔和重复传参。""" return _parse_csv_query_param(request, "designer_names") def _parse_csv_query_param(request, name: str): raw_values = request.query_params.getlist("state_names") if name != "state_names": raw_values = request.query_params.getlist(name) if not raw_values: return None values = [] for raw_value in raw_values: values.extend( item.strip() for item in str(raw_value).split(',') if item.strip() ) return values class DesignerWorkflowTaskSummaryView(SettlementVisibilityMixin, APIView): """ 设计师工序任务量统计 API GET /api/v1/settlement/workflows/designer-task-summary/ """ authentication_classes = PlateOrderSummaryView.authentication_classes permission_classes = [IsAuthenticated] view_all_permission = "printing.view_all_plateorders" def get(self, request): date_str = request.query_params.get("date") if not date_str: return Response( {"error": "缺少 date 参数"}, status=status.HTTP_400_BAD_REQUEST ) if not re.match(r"^\d{4}-\d{2}-\d{2}$", date_str): return Response( {"error": "日期格式错误,请使用 YYYY-MM-DD 格式"}, status=status.HTTP_400_BAD_REQUEST, ) try: settlement_date = date.fromisoformat(date_str) except ValueError: return Response({"error": "日期不存在"}, status=status.HTTP_403_FORBIDDEN) emp = getattr(request.user, "employee", None) if emp is None or emp.merchant is None: if request.user.is_superuser: merchant = Merchant.objects.first() if merchant is None: return Response( {"error": "系统中没有商户"}, status=status.HTTP_403_FORBIDDEN ) merchant_id = merchant.id else: return Response( {"error": "用户未关联商户"}, status=status.HTTP_403_FORBIDDEN ) else: merchant_id = emp.merchant.id state_names = _parse_state_names(request) designer_names = _parse_designer_names(request) try: data = get_designer_workflow_task_summary( merchant_id=merchant_id, settlement_date=settlement_date, user=self.get_service_user(request.user), state_names=state_names, designer_names=designer_names, ) return Response({ "data": data, "meta": { "date": settlement_date.isoformat(), "state_names": list(state_names or []), "state_filter": ( {"mode": "exact", "state_names": list(state_names)} if state_names is not None else {"mode": "suffix", "suffix": DEFAULT_DESIGNER_TASK_STATE_NAME_SUFFIX} ), "designer_names": list(designer_names or []), "designer_param_key": DEFAULT_DESIGNER_PARAM_KEY, "quantity_param_key": DEFAULT_DESIGNER_TASK_QUANTITY_PARAM_KEY, "time_param_key": DEFAULT_DESIGNER_TASK_TIME_PARAM_KEY, "empty_quantity_default": 1, "time_source": "parameters.完成时间; fallback=StateFlowRecord.completed_at", }, }) except ValueError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.exception(f"[settlement.views] 获取设计师工序任务量统计失败: {e}") return Response( {"error": "获取统计数据失败"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, )