// ==UserScript==
// @name SHEIN上传运单号2.0(终极完整版)
// @namespace https://docs.scriptcat.org/
// @version 3.0
// @description 导入订单+商品ID → 自动登录POD获取token → 自动查物流 → 有运单号才上传 → 暂停/继续 → 完整日志
// @author assistant
// @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;
}
// ===================== 登录功能 =====================
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; // 自动设置token
status.textContent = '✅ 登录成功!token已自动生效';
status.style.color = 'green';
addLog('POD 登录成功,已自动获取token', 'success');
} else {
status.textContent = '❌ ' + (json.msg || '登录失败');
status.style.color = 'red';
}
} catch (e) {
status.textContent = '❌ 登录异常:' + e.message;
status.style.color = 'red';
}
};
// 自动填入默认账号
window.onload = () => {
document.getElementById('loginMerchant').value = '灿纯';
document.getElementById('loginMobile').value = '';
document.getElementById('loginPwd').value = '';
};
// ===================== 加载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');
};
// ===================== 查询物流(使用登录后的token) =====================
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';
};
})();