"""期权复盘截图:独立命名空间,与合约同款四周期 5m/15m/1h/4h.""" from __future__ import annotations import json import os import re from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence OPTIONS_REVIEW_UPLOAD_TFS: tuple[str, ...] = ("5m", "15m", "1h", "4h") OPTIONS_REVIEW_ALLOWED_EXT = frozenset({".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"}) _DRAFT_ID_RE = re.compile(r"^[a-f0-9]{32}$") _SLOT_FILE_RE = re.compile( r"^options_journal_([a-f0-9]{32})_(5m|15m|1h|4h)\.(png|jpg|jpeg|webp|gif|bmp)$", re.I, ) def normalize_options_review_draft_id(raw: Any) -> Optional[str]: s = str(raw or "").strip().lower() if _DRAFT_ID_RE.match(s): return s return None def _safe_ext(filename: str) -> str: ext = os.path.splitext(str(filename or ""))[1].lower() return ext if ext in OPTIONS_REVIEW_ALLOWED_EXT else ".png" def options_review_upload_dir(base_upload_folder: str) -> str: """独立子目录 static/images/options_journal.""" base = os.path.abspath(base_upload_folder or "") path = os.path.join(base, "options_journal") os.makedirs(path, exist_ok=True) return path def build_options_review_slot_filename( draft_id: str, tf: str, ext: str, *, secure_filename_fn: Callable[[str], str], ) -> str: ext = ext if ext.startswith(".") else f".{ext}" ext = _safe_ext(f"x{ext}") fname = secure_filename_fn(f"options_journal_{draft_id}_{tf}{ext}") return fname or "" def is_valid_options_review_file(filename: str, draft_id: str, tf: str) -> bool: fn = os.path.basename(str(filename or "").strip()) if not fn or fn != str(filename or "").strip(): return False m = _SLOT_FILE_RE.match(fn) if not m: return False return m.group(1) == draft_id.lower() and m.group(2) == tf def save_options_review_slot_file( file, draft_id: str, tf: str, upload_folder: str, *, secure_filename_fn: Callable[[str], str], ) -> Optional[Dict[str, str]]: if tf not in OPTIONS_REVIEW_UPLOAD_TFS or not draft_id or not upload_folder: return None if not file or not getattr(file, "filename", None): return None ext = _safe_ext(file.filename) fname = build_options_review_slot_filename( draft_id, tf, ext, secure_filename_fn=secure_filename_fn ) if not fname: return None os.makedirs(upload_folder, exist_ok=True) path = os.path.join(upload_folder, fname) file.save(path) return {"tf": tf, "file": fname} def parse_options_review_images_json(raw: Any) -> List[Dict[str, str]]: if not raw: return [] if isinstance(raw, list): data = raw else: try: data = json.loads(str(raw)) except (TypeError, ValueError, json.JSONDecodeError): return [] if not isinstance(data, list): return [] out: List[Dict[str, str]] = [] for item in data: if not isinstance(item, dict): continue tf = str(item.get("tf") or "").strip() file = str(item.get("file") or "").strip() if file: out.append({"tf": tf, "file": file}) return out def images_json_dumps(items: Sequence[Mapping[str, str]]) -> Optional[str]: if not items: return None return json.dumps(list(items), ensure_ascii=False, separators=(",", ":")) def options_review_image_paths(row: Any, upload_folder: str) -> List[str]: upload_root = os.path.abspath(upload_folder or "") options_dir = options_review_upload_dir(upload_root) paths: List[str] = [] seen: set[str] = set() def _add(name: Optional[str]) -> None: if not name: return base = os.path.basename(str(name).strip()) if not base: return for folder in (options_dir, upload_root): p = os.path.abspath(os.path.join(folder, base)) if os.path.isfile(p) and p not in seen: seen.add(p) paths.append(p) return try: keys = row.keys() if hasattr(row, "keys") else () except Exception: keys = () images = parse_options_review_images_json( row["images_json"] if "images_json" in keys else getattr(row, "images_json", None) ) for item in images: _add(item.get("file")) if "image" in keys or hasattr(row, "image"): _add(row["image"] if "image" in keys else getattr(row, "image", None)) return paths