// ==UserScript== // @name SHEIN自动上传运单号 // @namespace https://docs.shenma.top // @version 5.0 // @description 自动登录POD + 本地存储账号 + 自动获取token + 自动上传运单 // @author binning // @match https://www.ecofengpod.com/* // @match https://sso.geiwohuo.com/* // @grant none // @noframes // ==/UserScript== (function () { 'use strict'; // ===================== 全局样式 ===================== const style = document.createElement('style'); style.innerHTML = ` #toolBtn { position: fixed; top: 20px; right: 20px; width: 40px; height: 40px; background: #007bff; color: white; border-radius: 50%; text-align: center; line-height: 40px; font-size: 20px; cursor: pointer; z-index: 999999; box-shadow: 0 0 10px rgba(0,0,0,0.3); } #toolPanel { position: fixed; top: 70px; right: 20px; z-index: 999998; display: none; width: 380px; background: #fff; border: 1px solid #ccc; border-radius: 10px; padding: 15px; box-shadow: 0 0 8px rgba(0,0,0,1); font-family: system-ui; } .title-row { display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; } #loginToggle { background:#6c63ff; color:#fff; border:none; padding:4px 8px; border-radius:6px; cursor:pointer; font-size:12px; } #loginPanel { display:none; margin-bottom:10px; padding:10px; background:#f0f7ff; border-radius:8px; } #loginPanel input { width:100%; padding:6px; margin:4px 0; box-sizing:border-box; border:1px solid #ccc; border-radius:4px; } #doLogin { background:#409eff; color:white; border:none; width:100%; padding:6px; border-radius:6px; cursor:pointer; margin-top:4px; } #loginStatus { font-size:12px; margin-top:6px; text-align:center; } .btn { width:100%; padding:9px; border:none; border-radius:6px; cursor:pointer; color:white; margin:6px 0; font-size:14px; } .btn-file { background:#2385ee; } .btn-start { background:#19be6b; } .btn-pause { background:#ff5722; } .btn-resume { background:#ff9800; } #fileInput { display:none; } #status { font-size:13px; margin-top:10px; color:#333; line-height:1.5; } /* 日志样式 */ #logContainer { margin-top:12px; padding:10px; height:220px; overflow-y:auto; background:#f7f7f7; border:1px solid #ddd; border-radius:6px; font-size:12px; line-height:1.4; white-space:pre-wrap; } .log-success { color:#0d7f2d; font-weight:bold; } .log-error { color:#d70000; font-weight:bold; } .log-warn { color:#ff8c00; font-weight:bold; } .log-skip { color:#1750d1; font-weight:bold; } `; document.head.appendChild(style); // ===================== 悬浮按钮 + 面板 ===================== const toolBtn = document.createElement('div'); toolBtn.id = 'toolBtn'; toolBtn.textContent = '+'; document.body.appendChild(toolBtn); const panel = document.createElement('div'); panel.id = 'toolPanel'; panel.innerHTML = `

SHEIN 自动上传运单号

请导入表格...
=== 执行日志 ===\n
`; document.body.appendChild(panel); // 切换显示隐藏 let isOpen = false; toolBtn.onclick = () => { isOpen = !isOpen; panel.style.display = isOpen ? 'block' : 'none'; toolBtn.textContent = isOpen ? '−' : '+'; }; // ===================== 登录面板开关 ===================== const loginToggle = document.getElementById('loginToggle'); const loginPanel = document.getElementById('loginPanel'); loginToggle.onclick = () => { const show = loginPanel.style.display !== 'block'; loginPanel.style.display = show ? 'block' : 'none'; loginToggle.textContent = show ? '关闭登录' : '登录POD'; }; // ===================== 核心配置 ===================== const CONFIG = { queryApi: 'https://crossdiy.ecofengpod.com/api/orders/getlist', queryToken: '', uploadApi: 'https://sso.geiwohuo.com/gsp/order/importSingleMultipleExpress', uploadSite: 'shein-es', delay: 800, loginApi: 'https://crossdiy.ecofengpod.com/api/user/login' }; // 全局状态 let orderList = []; let XLSX = null; let isPaused = false; let currentIndex = 0; let success = 0; let fail = 0; let skip = 0; // ===================== 日志输出 ===================== function addLog(text, type = 'info') { const logEl = document.getElementById('logContainer'); const time = new Date().toLocaleTimeString(); let className = ''; if (type === 'success') className = 'log-success'; if (type === 'error') className = 'log-error'; if (type === 'warn') className = 'log-warn'; if (type === 'skip') className = 'log-skip'; const line = `[${time}] ${text}`; logEl.innerHTML += line + '\n'; logEl.scrollTop = logEl.scrollHeight; } // ===================== 本地存储 ===================== const STORAGE_KEY = 'pod_login_info'; function saveLoginInfo(info) { localStorage.setItem(STORAGE_KEY, JSON.stringify(info)); } function loadLoginInfo() { const data = localStorage.getItem(STORAGE_KEY); return data ? JSON.parse(data) : null; } // 页面加载时自动填充 window.onload = () => { const info = loadLoginInfo(); if (info) { document.getElementById('loginMerchant').value = info.merchant || ''; document.getElementById('loginMobile').value = info.mobile || ''; document.getElementById('loginPwd').value = info.password || ''; } }; // ===================== 登录功能 ===================== document.getElementById('doLogin').onclick = async () => { const merchant = document.getElementById('loginMerchant').value.trim(); const mobile = document.getElementById('loginMobile').value.trim(); const pwd = document.getElementById('loginPwd').value.trim(); const status = document.getElementById('loginStatus'); if (!merchant || !mobile || !pwd) { status.textContent = '请填写完整信息'; status.style.color = 'red'; return; } status.textContent = '登录中...'; status.style.color = '#007bff'; try { const res = await fetch(CONFIG.loginApi, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ merchant, mobile, password: pwd }) }); const json = await res.json(); if (json.code === 1 && json.data?.userinfo?.token) { const token = json.data.userinfo.token; CONFIG.queryToken = token; status.textContent = '✅ 登录成功!token已自动生效'; status.style.color = 'green'; addLog('POD 登录成功', 'success'); // 保存到本地 saveLoginInfo({ merchant, mobile, password: pwd }); } else { status.textContent = '❌ ' + (json.msg || '登录失败'); status.style.color = 'red'; } } catch (e) { status.textContent = '❌ 登录异常:' + e.message; status.style.color = 'red'; } }; // ===================== 加载Excel库 ===================== async function loadXLSX() { if (window.XLSX) return XLSX = window.XLSX; return new Promise(resolve => { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js'; script.onload = () => { XLSX = window.XLSX; resolve(); }; document.head.appendChild(script); }); } // ===================== 读取表格 ===================== document.getElementById('selectFile').onclick = () => { document.getElementById('fileInput').click(); }; document.getElementById('fileInput').onchange = async (e) => { const status = document.getElementById('status'); if (!e.target.files.length) return; await loadXLSX(); status.textContent = '正在读取表格...'; const file = e.target.files[0]; const reader = new FileReader(); reader.readAsArrayBuffer(file); reader.onload = (ev) => { try { const data = new Uint8Array(ev.target.result); const workbook = XLSX.read(data, { type: 'array', raw: true }); const sheet = workbook.Sheets[workbook.SheetNames[0]]; const json = XLSX.utils.sheet_to_json(sheet, { header: 1 }); let rows = json.filter(r => r && r.length > 0); if (rows.length < 2) throw new Error('表格数据无效'); let header = rows[1].map(h => (h || '').toString().trim()); let dataRows = rows.slice(2); let orderIdx = header.findIndex(col => col.includes('订单号')); let goodsIdx = header.findIndex(col => col.includes('商品ID')); if (orderIdx === -1 || goodsIdx === -1) { throw new Error('未找到【订单号】或【商品ID】列'); } orderList = []; dataRows.forEach(row => { let orderNo = (row[orderIdx] || '').toString().trim(); let goodsId = (row[goodsIdx] || '').toString().trim(); if (orderNo && goodsId) { orderList.push({ orderNo, goodsId }); } }); if (orderList.length === 0) { status.textContent = '❌ 未读取到有效数据'; document.getElementById('startTask').disabled = true; return; } status.textContent = `✅ 读取成功:共 ${orderList.length} 条订单`; addLog(`成功导入 ${orderList.length} 条订单`, 'success'); document.getElementById('startTask').disabled = false; } catch (err) { status.textContent = '❌ 读取失败:' + err.message; addLog(`读取失败:${err.message}`, 'error'); } }; }; // ===================== 暂停按钮 ===================== const pauseBtn = document.getElementById('pauseBtn'); pauseBtn.onclick = () => { isPaused = !isPaused; pauseBtn.textContent = isPaused ? '继续执行' : '暂停执行'; pauseBtn.className = isPaused ? 'btn btn-resume' : 'btn btn-pause'; const msg = isPaused ? '已暂停' : '已继续'; document.getElementById('status').textContent = `状态:${msg}`; addLog(msg, 'warn'); }; // ===================== 查询物流 ===================== async function queryLogistics(orderNo) { if (!CONFIG.queryToken) { addLog('⚠️ 请先登录POD', 'warn'); return null; } const params = new URLSearchParams({ third_no: orderNo, pageNumber: 1, pageSize: 100, status: '-2,-1,0,1,2,3,6' }); try { const res = await fetch(`${CONFIG.queryApi}?${params}`, { method: 'GET', headers: { 'Token': CONFIG.queryToken, 'Content-Type': 'application/json' } }); const json = await res.json(); if (!json.data?.list?.length) return null; const item = json.data.list[0]; const logisticsNo = (item.logistics_no || '').trim(); const logistics = (item.logistics || '').trim(); if (logisticsNo) { return { expressIdCode: logistics, expressCode: logisticsNo }; } return null; } catch (e) { return null; } } // ===================== 上传运单 ===================== async function uploadLogistics(orderNo, goodsId, expressCode, expressIdCode) { const res = await fetch(CONFIG.uploadApi, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderNo: orderNo, site: CONFIG.uploadSite, orderGoodsExpressInfos: [{ expressCode: expressCode, expressIdCode: expressIdCode, goodsId: Number(goodsId) }] }), credentials: 'include' }); return await res.json(); } // ===================== 暂停等待 ===================== function delayWithPause(ms) { return new Promise(resolve => { const check = () => { if (isPaused) { setTimeout(check, 200); } else { setTimeout(resolve, ms); } }; check(); }); } // ===================== 执行任务 ===================== document.getElementById('startTask').onclick = async function startRun() { const btn = document.getElementById('startTask'); const status = document.getElementById('status'); if (!CONFIG.queryToken) { alert('请先点击【登录POD】完成登录!'); return; } if (!isPaused) { currentIndex = 0; success = 0; fail = 0; skip = 0; document.getElementById('logContainer').innerHTML = '=== 执行日志 ===\n'; addLog('开始执行任务...', 'info'); } btn.disabled = true; btn.textContent = '执行中'; pauseBtn.disabled = false; for (; currentIndex < orderList.length; currentIndex++) { const { orderNo, goodsId } = orderList[currentIndex]; const curr = currentIndex + 1; const total = orderList.length; try { status.textContent = `[${curr}/${total}] 查询:${orderNo}`; await delayWithPause(100); const logisticsInfo = await queryLogistics(orderNo); if (!logisticsInfo) { addLog(`⏭️ 订单 ${orderNo}:无有效运单号,已跳过`, 'skip'); skip++; await delayWithPause(CONFIG.delay); continue; } const uploadRes = await uploadLogistics( orderNo, goodsId, logisticsInfo.expressCode, logisticsInfo.expressIdCode ); if (uploadRes.success === true) { addLog(`✅ 订单 ${orderNo}:上传成功 | 运单:${logisticsInfo.expressCode}`, 'success'); success++; } else { addLog(`❌ 订单 ${orderNo}:上传失败 | ${uploadRes.msg || '未知原因'}`, 'error'); fail++; } } catch (err) { addLog(`⚠️ 订单 ${orderNo}:异常 → ${err.message}`, 'warn'); fail++; } status.textContent = `进度:${curr}/${total} | 成功:${success} | 失败:${fail} | 跳过:${skip}`; await delayWithPause(CONFIG.delay); } status.textContent = `🎉 任务完成!成功:${success} | 失败:${fail} | 跳过:${skip}`; addLog(`=== 任务全部结束 === 成功:${success} 失败:${fail} 跳过:${skip}`, 'success'); btn.textContent = '开始自动执行'; btn.disabled = false; pauseBtn.disabled = true; isPaused = false; pauseBtn.textContent = '暂停执行'; pauseBtn.className = 'btn btn-pause'; }; })();