淘宝定时下单

淘宝、天猫定时自动结算与提交订单。支持在页面面板直接设置时间并锁定商品,到点自动进行下单流程,不干扰平时正常购物。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         TaoTimer
// @name:zh-CN   淘宝定时下单
// @namespace    https://github.com/Miracle3310/TaoTimer
// @version      3.3.1
// @description  Schedule checkout and order submission on Taobao/Tmall from a local page panel.
// @description:zh-CN 淘宝、天猫定时自动结算与提交订单。支持在页面面板直接设置时间并锁定商品,到点自动进行下单流程,不干扰平时正常购物。
// @author       Miracle3310
// @license      MIT
// @homepageURL  https://greasyfork.org/zh-CN/scripts/578859-taotimer
// @supportURL   https://github.com/Miracle3310/TaoTimer/issues
// @match        *://cart.taobao.com/*
// @match        *://buy.taobao.com/*
// @match        *://buy.tmall.com/*
// @grant        GM_deleteValue
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function () {
  "use strict";

  (() => {
    const BUYING_WINDOW_SECONDS = 5;
    const START_EARLY_MS = 10_000;
    const SUBMIT_TIMEOUT_MS = 15_000;
    const SUBMIT_RETRY_DELAY_MS = 2_000;
    const MAX_SUBMIT_ATTEMPTS = 3;
    const DRY_RUN = false;
    const AUTO_CHECKOUT = true;
    const AUTO_SUBMIT = true;

    const ROW_SELECTOR = "[class*='cartItemInfoContainer']";
    const ANT_WRAP = "label.ant-checkbox-wrapper";
    const ANT_INPUT = "input.ant-checkbox-input";
    const CHECKOUT_CANDIDATES = ["[class*='btn--']", "button", "div", "a"];
    const ORDER_SUBMIT_MATCH_TEXT = /提\s*交\s*订\s*单|确\s*认\s*订\s*单|提\s*交\s*并\s*支\s*付/;
    const ORDER_SUBMIT_CANDIDATES = [
      ".go-btn",
      "[title*='提交订单']",
      "[aria-label*='提交订单']",
      "[class*='submit-btn']",
      "[class*='SubmitBtn']",
      "[class*='btn--']",
      "[role='button']",
      "button",
      "a",
    ];
    const RISK_CONTROL_TEXT = /验证码|滑块|安全验证|操作频繁|访问受限|异常流量|请完成验证|环境异常|稍后再试|检测到.{0,12}风险|存在.{0,12}风险/;
    const AUTO_SUBMIT_KEY = "taotimer_auto_submit";

    const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

    const getStoredValue = (key, fallback = null) => {
      try {
        if (typeof GM_getValue === "function") return GM_getValue(key, fallback);
      } catch (e) {}
      return sessionStorage.getItem(key) ?? fallback;
    };

    const setStoredValue = (key, value) => {
      try {
        if (typeof GM_setValue === "function") {
          GM_setValue(key, value);
          return;
        }
      } catch (e) {}
      sessionStorage.setItem(key, String(value));
    };

    const deleteStoredValue = (key) => {
      try {
        if (typeof GM_deleteValue === "function") {
          GM_deleteValue(key);
          return;
        }
      } catch (e) {}
      sessionStorage.removeItem(key);
    };

    const isClickable = (el) => {
      if (!el) return false;
      const s = getComputedStyle(el);
      if (s.display === "none" || s.visibility === "hidden" || s.opacity === "0" || s.pointerEvents === "none") return false;
      if (el.disabled || el.getAttribute("aria-disabled") === "true" || el.getAttribute("disabled") !== null) return false;
      if (/\b(disabled|loading)\b/i.test(el.className || "")) return false;
      const rect = el.getBoundingClientRect();
      if (rect.width <= 0 || rect.height <= 0) return false;
      return true;
    };

    const hasText = (el, re) => {
      const text = [el.textContent, el.getAttribute("title"), el.getAttribute("aria-label")]
        .filter(Boolean)
        .join(" ")
        .trim();
      return re.test(text);
    };

    const findAllByText = (candidates, re) => {
      const matches = [];
      for (const sel of candidates) {
        try {
          for (const el of document.querySelectorAll(sel)) {
            if (hasText(el, re)) matches.push(el);
          }
        } catch (e) {
          continue;
        }
      }
      return [...new Set(matches)];
    };

    const findByText = (candidates, re) => {
      return findAllByText(candidates, re)[0] || null;
    };

    const isCovered = (el) => {
      const rect = el.getBoundingClientRect();
      const x = rect.left + rect.width / 2;
      const y = rect.top + rect.height / 2;
      const topEl = document.elementFromPoint(x, y);
      return topEl && topEl !== el && !el.contains(topEl);
    };

    const isReadyToClick = (el) => isClickable(el) && !isCovered(el);

    const findSubmitButton = () => {
      const candidates = [
        ...document.querySelectorAll(".go-btn, [title*='提交订单'], [aria-label*='提交订单'], [class*='submit-btn'], [class*='SubmitBtn']"),
        ...findAllByText(ORDER_SUBMIT_CANDIDATES, ORDER_SUBMIT_MATCH_TEXT),
      ];
      return [...new Set(candidates)]
        .filter((el) => hasText(el, ORDER_SUBMIT_MATCH_TEXT) || /go-btn|submit-btn|SubmitBtn/.test(el.className || ""))
        .filter(isReadyToClick)
        .sort((a, b) => {
          const ar = a.getBoundingClientRect();
          const br = b.getBoundingClientRect();
          return br.bottom - ar.bottom || br.right - ar.right;
        })[0] || null;
    };

    const updateConfirmStatus = (message, color = "#ff5000") => {
      let panel = document.getElementById("taotimer-confirm-status");
      if (!panel) {
        panel = document.createElement("div");
        panel.id = "taotimer-confirm-status";
        panel.style.cssText = `
          position: fixed; top: 120px; right: 20px; z-index: 999999;
          background: #fff; border: 2px solid #ff5000; border-radius: 8px;
          padding: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); width: 260px;
          font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; font-size: 13px; color: #333;
        `;
        document.body.appendChild(panel);
      }
      panel.style.borderColor = color;
      panel.innerText = message;
    };

    const hasRiskControlPrompt = () => {
      const pageText = (document.body && document.body.innerText) || "";
      const statusText = (document.getElementById("taotimer-confirm-status") || {}).innerText || "";
      return RISK_CONTROL_TEXT.test(pageText.replace(statusText, ""));
    };

    const cleanupAutoSubmit = () => {
      deleteStoredValue(AUTO_SUBMIT_KEY);
      sessionStorage.removeItem(AUTO_SUBMIT_KEY);
    };

    const forceClick = (element) => {
      if (!element) return;
      console.log("[taotimer] Executing force click on:", element);
      element.scrollIntoView({ block: "center", inline: "center" });
      try {
        element.focus();
        element.click();
      } catch (e) {}
      const dispatchMouseEvent = (type) => {
        const rect = element.getBoundingClientRect();
        element.dispatchEvent(new MouseEvent(type, {
          view: window,
          bubbles: true,
          cancelable: true,
          clientX: rect.left + rect.width / 2,
          clientY: rect.top + rect.height / 2,
        }));
      };
      dispatchMouseEvent("mousedown");
      dispatchMouseEvent("mouseup");
      dispatchMouseEvent("click");
    };

    const getRowSignature = (row) => {
      const link = row.querySelector("a");
      if (link && link.href) {
        const match = link.href.match(/[?&]id=(\d+)/);
        if (match) return match[1];
      }
      return row.textContent.replace(/\s+/g, "").substring(0, 50);
    };

    async function attemptFromCart() {
      const rows = Array.from(document.querySelectorAll(ROW_SELECTOR));
      if (!rows.length) return { progressed: false };

      const targets = JSON.parse(sessionStorage.getItem("taotimer_targets") || "[]");
      let itemReady = false;

      for (const row of rows) {
        const input = row.querySelector(ANT_INPUT);
        const wrapper = row.querySelector(ANT_WRAP);
        const sig = getRowSignature(row);

        if (targets.includes(sig)) {
          if (input && !input.checked) {
            if (!DRY_RUN && wrapper) wrapper.click();
            await sleep(100);
          }
          itemReady = true;
          row.style.outline = "3px solid limegreen";
        } else if (input && input.checked) {
          if (!DRY_RUN && wrapper) wrapper.click();
          await sleep(50);
        }
      }

      if (!itemReady) return { progressed: false };
      if (DRY_RUN || !AUTO_CHECKOUT) return { progressed: false };

      const checkoutBtn = findByText(CHECKOUT_CANDIDATES, /结算/);
      if (!isClickable(checkoutBtn)) return { progressed: false };

      console.log("[taotimer] Item targeted. Clicking checkout button.");

      setStoredValue(AUTO_SUBMIT_KEY, true);
      checkoutBtn.click();
      return { progressed: true };
    }

    async function aggressiveRefreshLoop(deadline) {
      const result = await attemptFromCart();
      if (result.progressed) {
        sessionStorage.removeItem("taotimer_windowEnd");
        sessionStorage.removeItem("taotimer_active");
        return;
      }

      if (Date.now() < deadline) {
        await sleep(300 + Math.random() * 200);
        location.reload();
      } else {
        sessionStorage.removeItem("taotimer_windowEnd");
        sessionStorage.removeItem("taotimer_active");
        if (document.getElementById("taotimer-status")) updateUI();
      }
    }

    async function scheduleCartChecker() {
      const targetTimeStr = sessionStorage.getItem("taotimer_time");
      if (!targetTimeStr) return;

      const [hh, mm, ss] = targetTimeStr.split(":").map(Number);
      let target = new Date();
      target.setHours(hh, mm, ss || 0, 0);
      if (target <= new Date()) target.setDate(target.getDate() + 1);

      const checkLoop = setInterval(async () => {
        if (sessionStorage.getItem("taotimer_active") !== "true") return clearInterval(checkLoop);

        const msUntilTarget = target - new Date();
        const statusEl = document.getElementById("taotimer-status");
        if (statusEl) statusEl.innerText = `状态: 倒计时 ${(msUntilTarget / 1000).toFixed(1)} 秒...`;

        if (msUntilTarget <= START_EARLY_MS && msUntilTarget > 0) {
          clearInterval(checkLoop);
          if (statusEl) statusEl.innerText = "状态: 准备进入刷新窗口...";

          while (new Date() < target) {
            if (sessionStorage.getItem("taotimer_active") !== "true") return;
            await sleep(50);
          }

          const deadline = Date.now() + BUYING_WINDOW_SECONDS * 1000;
          sessionStorage.setItem("taotimer_windowEnd", deadline);
          aggressiveRefreshLoop(deadline);
        }
      }, 500);
    }

    function updateUI() {
      const isActive = sessionStorage.getItem("taotimer_active") === "true";
      const startBtn = document.getElementById("taotimer-start-btn");
      const stopBtn = document.getElementById("taotimer-stop-btn");
      const statusEl = document.getElementById("taotimer-status");
      if (!statusEl) return;

      if (isActive) {
        startBtn.style.display = "none";
        stopBtn.style.display = "block";
        statusEl.innerText = `状态: 已设时间 ${sessionStorage.getItem("taotimer_time")},等待中...`;
        statusEl.style.color = "#ff5000";
      } else {
        startBtn.style.display = "block";
        stopBtn.style.display = "none";
        statusEl.innerText = "状态: 未启动";
        statusEl.style.color = "#666";
      }
    }

    function createUI() {
      if (document.getElementById("taotimer-panel")) return;

      const panel = document.createElement("div");
      panel.id = "taotimer-panel";
      panel.style.cssText = `
        position: fixed; top: 120px; right: 20px; z-index: 999999;
        background: #fff; border: 2px solid #ff5000; border-radius: 8px;
        padding: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); width: 260px;
        font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; font-size: 13px; color: #333;
      `;

      panel.innerHTML = `
        <div style="font-weight:bold; color:#ff5000; margin-bottom:12px; font-size: 15px; text-align: center;">淘宝定时下单控制台</div>
        <div style="margin-bottom:10px; line-height: 1.5;">
          <span style="display:inline-block; background:#ffe4d0; color:#ff5000; padding:2px 6px; border-radius:4px; margin-bottom:4px;">步骤 1</span><br>
          在左侧购物车中勾选你要定时下单的商品。
        </div>
        <div style="margin-bottom:15px; line-height: 1.5;">
          <span style="display:inline-block; background:#ffe4d0; color:#ff5000; padding:2px 6px; border-radius:4px; margin-bottom:4px;">步骤 2</span><br>
          设定定时下单时间 (时:分:秒):
          <input type="time" step="1" id="taotimer-time-input" style="width:100%; margin-top:6px; padding:6px; border:1px solid #ccc; border-radius:4px; box-sizing: border-box;">
        </div>
        <button id="taotimer-start-btn" style="width:100%; padding:10px; background:linear-gradient(90deg, #ff9000, #ff5000); color:white; border:none; border-radius:4px; font-weight:bold; cursor:pointer; font-size: 14px;">锁定商品并开始</button>
        <button id="taotimer-stop-btn" style="width:100%; padding:10px; background:#f0f0f0; color:#333; border:1px solid #ddd; border-radius:4px; font-weight:bold; cursor:pointer; font-size: 14px; display:none;">取消定时</button>
        <div id="taotimer-status" style="margin-top:12px; font-weight:bold; text-align: center;">状态: 未启动</div>
      `;
      document.body.appendChild(panel);

      const savedTime = sessionStorage.getItem("taotimer_time");
      if (savedTime) document.getElementById("taotimer-time-input").value = savedTime;

      document.getElementById("taotimer-start-btn").onclick = () => {
        const timeVal = document.getElementById("taotimer-time-input").value;
        if (!timeVal) return alert("请输入定时时间!注意要包含秒。");

        const rows = Array.from(document.querySelectorAll(ROW_SELECTOR));
        const targets = [];
        for (const row of rows) {
          const input = row.querySelector(ANT_INPUT);
          if (input && input.checked) targets.push(getRowSignature(row));
        }

        if (targets.length === 0) return alert("请先在购物车中勾选你要定时下单的商品!");

        sessionStorage.setItem("taotimer_active", "true");
        sessionStorage.setItem("taotimer_time", timeVal);
        sessionStorage.setItem("taotimer_targets", JSON.stringify(targets));

        updateUI();
        scheduleCartChecker();
        alert("已锁定目标商品并开启倒计时!请保持此页面打开。");
      };

      document.getElementById("taotimer-stop-btn").onclick = () => {
        sessionStorage.removeItem("taotimer_active");
        sessionStorage.removeItem("taotimer_windowEnd");
        cleanupAutoSubmit();
        updateUI();
      };

      updateUI();
    }

    function handleConfirmPage() {
      if (getStoredValue(AUTO_SUBMIT_KEY, false) !== true && getStoredValue(AUTO_SUBMIT_KEY, "false") !== "true") {
        console.log("[taotimer] Normal manual checkout detected. Script sleeps.");
        return;
      }

      console.log("[taotimer] Auto checkout detected. Looking for submit button.");
      if (DRY_RUN || !AUTO_SUBMIT) return;

      updateConfirmStatus("TaoTimer: 已进入订单确认页,正在等待提交按钮可用...");

      let clickAttempts = 0;
      let lastClickAt = 0;
      let stopped = false;

      const fireClick = () => {
        if (stopped) return;

        if (hasRiskControlPrompt()) {
          stopped = true;
          cleanupAutoSubmit();
          updateConfirmStatus("TaoTimer: 检测到验证、频繁操作或风险提示,已停止自动提交,请手动处理。", "#d93025");
          console.warn("[taotimer] Risk-control or verification prompt detected. Auto submit stopped.");
          observer.disconnect();
          clearInterval(pollingId);
          return;
        }

        if (clickAttempts >= MAX_SUBMIT_ATTEMPTS) {
          stopped = true;
          cleanupAutoSubmit();
          updateConfirmStatus("TaoTimer: 已多次尝试提交但页面未继续跳转,请手动确认订单状态。", "#d93025");
          observer.disconnect();
          clearInterval(pollingId);
          return;
        }

        if (Date.now() - lastClickAt < SUBMIT_RETRY_DELAY_MS) return;

        const submitBtn = findSubmitButton();
        if (submitBtn) {
          clickAttempts += 1;
          lastClickAt = Date.now();
          updateConfirmStatus(`TaoTimer: 提交按钮已可用,正在尝试提交 (${clickAttempts}/${MAX_SUBMIT_ATTEMPTS})...`);

          console.log("[taotimer] Submit button is ready. Clicking now:", submitBtn);
          forceClick(submitBtn);
        } else {
          updateConfirmStatus("TaoTimer: 正在等待提交按钮可用,若页面要求补充信息请手动处理...");
        }
      };

      window.addEventListener("beforeunload", cleanupAutoSubmit, { once: true });

      const pollingId = setInterval(fireClick, 250);
      const observer = new MutationObserver(fireClick);
      observer.observe(document.body, { childList: true, subtree: true });

      setTimeout(() => {
        observer.disconnect();
        clearInterval(pollingId);
        if (!stopped) {
          cleanupAutoSubmit();
          updateConfirmStatus("TaoTimer: 15 秒内未能完成自动提交,请手动确认订单状态。", "#d93025");
          console.error("[taotimer] Failed: submit button was not usable within timeout.");
        }
      }, SUBMIT_TIMEOUT_MS);

      fireClick();
    }

    function main() {
      console.log("[taotimer] Script loaded on:", location.host + location.pathname);

      if (location.host.includes("cart.taobao.com") || location.pathname.includes("cart.htm")) {
        createUI();
        const windowEnd = sessionStorage.getItem("taotimer_windowEnd");
        if (windowEnd && Date.now() < parseInt(windowEnd, 10) && sessionStorage.getItem("taotimer_active") === "true") {
          aggressiveRefreshLoop(parseInt(windowEnd, 10));
        } else if (sessionStorage.getItem("taotimer_active") === "true") {
          scheduleCartChecker();
        }
      } else if (location.host.includes("buy.taobao.com") || location.host.includes("buy.tmall.com")) {
        handleConfirmPage();
      }
    }

    main();
  })();
})();