client-ip.ts 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. /**
  2. * 取本机对外 IP —— 用于 DMS 的**访客归属**(用户 2026-09-22 要求)。
  3. *
  4. * 接口:`GET {VITE_IP_API}/proxy/verify/getIPByBrowser`
  5. * 返回**纯文本 IP**(响应头 Content-Type 标的是 `application/json`,但正文就是裸 IP,
  6. * 实测 `116.232.248.128`)—— 所以**按 text 读、不要 JSON.parse**。
  7. *
  8. * ⚠️ **必须走同源代理**:接口是 `http://121.43.55.7:10011`,而页面是 HTTPS。
  9. * 接口本身带 `Access-Control-Allow-Origin: *`(跨域不成问题),但**混合内容**会被浏览器
  10. * 直接拦掉 —— 与聊天后端同一类问题。所以 dev 走 vite 代理(`vite.config.ts` 的 `/ip-api/`),
  11. * 生产需要 nginx 等价转发。
  12. *
  13. * 拿不到(没配代理 / 超时 / 返回不是 IP)一律返回空串、**不抛异常**:
  14. * 调用方会退回旧的「访客_<浏览器随机 id>」——**绝不能因为取不到 IP 就写不进 DMS**。
  15. */
  16. import { getClientIpApiBaseUrl } from "@/utils/runtime-config";
  17. /** 进程内缓存:一次会话里 IP 不会变,没必要每轮都问 */
  18. let cachedIp = "";
  19. /** 并发去重:多个写入同时要 IP 时只发一次请求 */
  20. let inflight: Promise<string> | null = null;
  21. /** 形如 `1.2.3.4` 才算数(IPv6 也放行:含冒号的裸地址) */
  22. const isIpLike = (text: string): boolean =>
  23. /^\d{1,3}(\.\d{1,3}){3}$/.test(text) || /^[0-9a-f:]{6,}$/i.test(text);
  24. /** 已经拿到的 IP(没拿到就是空串)。同步可用,给不方便 await 的地方读缓存。 */
  25. export const getCachedClientIp = (): string => cachedIp;
  26. /** 取 IP(并发调用只发一次请求;失败返回空串,下次调用会重试) */
  27. export const ensureClientIp = (): Promise<string> => {
  28. if (cachedIp) return Promise.resolve(cachedIp);
  29. if (inflight) return inflight;
  30. const base = getClientIpApiBaseUrl();
  31. if (!base) {
  32. return Promise.resolve(""); // 没配代理:静默退回随机 id,不打扰用户
  33. }
  34. inflight = (async () => {
  35. const controller = new AbortController();
  36. const timer = setTimeout(() => controller.abort(), 3000);
  37. try {
  38. const res = await fetch(`${base}/proxy/verify/getIPByBrowser`, { signal: controller.signal });
  39. const text = (await res.text()).trim();
  40. if (isIpLike(text)) {
  41. cachedIp = text;
  42. } else {
  43. console.warn("[client-ip] 返回的内容不像 IP,已忽略:", JSON.stringify(text.slice(0, 40)));
  44. }
  45. } catch (err) {
  46. console.warn("[client-ip] 取 IP 失败(DMS 访客归属退回浏览器随机 id):", (err as Error)?.message || err);
  47. } finally {
  48. clearTimeout(timer);
  49. inflight = null;
  50. }
  51. return cachedIp;
  52. })();
  53. return inflight;
  54. };