minio_url.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. """MinIO 附件 URL 规范化(数据迁移共用)。
  2. 背景
  3. ----
  4. CRM / 地磅 / 报销与 shanxia2.0 共用同一 MinIO(bucket 默认 ``ruoyi``),
  5. 物理对象无需复制。旧 CRM ``sys_oss.url`` 存的是带域名的绝对 URL(含历史
  6. ``47.99.164.66:20004``、``121.43.228.102:9000`` 等),门户 ``sys_oss.url`` 与
  7. 业务表字段存 **相对 object key**(如 ``2025/10/08/xxx.png``)。
  8. 旧表 ``sys_oss.file_name`` 通常已是 object key,与 ``url`` 中的路径一致,
  9. 解析失败时可优先回退 ``file_name``。
  10. 报销 ``bx_attachment.file_path`` 本身即为相对路径(如 ``bx/2026-05-19/...``),
  11. 可直接写入目标库。
  12. """
  13. from __future__ import annotations
  14. from typing import Iterable, Optional
  15. from urllib.parse import unquote, urlparse
  16. DEFAULT_BUCKET = "ruoyi"
  17. # 历史绝对 URL 前缀(域名/网关变更);匹配时去掉前缀得到 object key
  18. LEGACY_URL_PREFIXES = (
  19. "http://47.99.164.66:20004/ruoyi/",
  20. "https://47.99.164.66:20004/ruoyi/",
  21. "http://121.43.228.102:9000/ruoyi/",
  22. "https://121.43.228.102:9000/ruoyi/",
  23. "http://www.chengyantek.cn:20004/ruoyi/",
  24. "https://www.chengyantek.cn:20004/ruoyi/",
  25. "http://www.chengyantek.cn/ruoyi/",
  26. "https://www.chengyantek.cn/ruoyi/",
  27. "http://www.chengyantek.cn/zkxw/",
  28. "https://www.chengyantek.cn/zkxw/",
  29. )
  30. def strip_url_query(raw: str) -> str:
  31. """去掉预签名 query(``?X-Amz-...``)。"""
  32. value = (raw or "").strip()
  33. if not value:
  34. return ""
  35. return value.split("?", 1)[0].strip()
  36. def _strip_legacy_prefix(url: str) -> Optional[str]:
  37. normalized = strip_url_query(url)
  38. for prefix in LEGACY_URL_PREFIXES:
  39. if normalized.startswith(prefix):
  40. return normalized[len(prefix) :].lstrip("/")
  41. return None
  42. def extract_minio_object_key(
  43. raw_url: Optional[str],
  44. *,
  45. bucket: str = DEFAULT_BUCKET,
  46. file_name_fallback: Optional[str] = None,
  47. ) -> str:
  48. """将绝对/相对 URL 规范为 MinIO object key(不含 bucket)。"""
  49. url = strip_url_query(raw_url or "")
  50. if not url:
  51. fallback = (file_name_fallback or "").strip().lstrip("/")
  52. return fallback
  53. legacy = _strip_legacy_prefix(url)
  54. if legacy:
  55. return legacy
  56. if url.startswith("http://") or url.startswith("https://"):
  57. parsed = urlparse(url)
  58. path = unquote((parsed.path or "").lstrip("/"))
  59. bucket_prefix = f"{bucket}/"
  60. if path.startswith(bucket_prefix):
  61. return path[len(bucket_prefix) :]
  62. # 少数 URL 路径不含 bucket 段
  63. return path
  64. return url.lstrip("/")
  65. def normalize_oss_storage_url(
  66. url: Optional[str],
  67. file_name: Optional[str] = None,
  68. *,
  69. bucket: str = DEFAULT_BUCKET,
  70. ) -> str:
  71. """迁移 CRM ``sys_oss`` 行:``url`` + ``file_name`` → 门户相对路径。"""
  72. from_url = extract_minio_object_key(url, bucket=bucket)
  73. if from_url:
  74. return from_url
  75. return (file_name or "").strip().lstrip("/")
  76. def join_relative_urls(urls: Iterable[str]) -> str:
  77. """逗号拼接相对路径(去空、去重保序)。"""
  78. seen = set()
  79. parts: list[str] = []
  80. for item in urls:
  81. key = (item or "").strip().lstrip("/")
  82. if not key or key in seen:
  83. continue
  84. seen.add(key)
  85. parts.append(key)
  86. return ",".join(parts)
  87. def resolve_oss_id_csv(
  88. oss_id_csv: Optional[str],
  89. oss_id_to_relative_url: dict[str, str],
  90. ) -> str:
  91. """业务表 OSS ID 串 → 相对 URL 串(用于客户证照、跟进附件等)。"""
  92. if not oss_id_csv:
  93. return ""
  94. urls: list[str] = []
  95. for token in str(oss_id_csv).split(","):
  96. oss_id = token.strip()
  97. if not oss_id:
  98. continue
  99. mapped = oss_id_to_relative_url.get(oss_id)
  100. if mapped:
  101. urls.append(mapped)
  102. return join_relative_urls(urls)
  103. if __name__ == "__main__":
  104. samples = [
  105. (
  106. "http://47.99.164.66:20004/ruoyi/2025/10/08/5ac0966ab3a74eadab571ce0bb3b441a.png",
  107. "2025/10/08/5ac0966ab3a74eadab571ce0bb3b441a.png",
  108. ),
  109. (
  110. "http://121.43.228.102:9000/ruoyi/2025/11/01/0048348b3d6c439f99db454c8c05e59a.gif?X-Amz-Algorithm=AWS4",
  111. "2025/11/01/0048348b3d6c439f99db454c8c05e59a.gif",
  112. ),
  113. ("2025/10/17/d601338282a44cf092bf68afe1aef239.jpeg", "2025/10/17/d601338282a44cf092bf68afe1aef239.jpeg"),
  114. ("bx/2026-05-19/4ca4ffbeb7fc4e22913936d0860b2e2d.png", "bx/2026-05-19/4ca4ffbeb7fc4e22913936d0860b2e2d.png"),
  115. ]
  116. for raw, expected in samples:
  117. got = extract_minio_object_key(raw)
  118. assert got == expected, f"{raw!r} -> {got!r}, want {expected!r}"
  119. print("minio_url self-check ok")