Schedule checkout and order submission on Taobao/Tmall from a local page panel.
// ==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();
})();
})();