// ==UserScript==
// @name 聚水团批量操作
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 1.批量采购 2.批量查库存
// @author jbn
// @match https://sc.scm121.com/*
// @run-at document-start
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_download
// @require https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js
// @license MIT
// ==/UserScript==
(function () {
const scriptVersion = GM_info.script.version;
let API_FIX_HEADERS = {
"Content-Type": "application/json;charset=UTF-8",
accept: "application/json",
"app-version": null,
appcode: null,
gwfp: null,
source: null,
Authorization: null
};
let supplierList = [];
let logDom
let selectedSupplierId
const PANEL_ID = "jushuituan_batch_cart_panel";
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function (resource, init) {
let url;
let headersRaw;
// 兼容两种入参:字符串url / Request对象
if (resource instanceof unsafeWindow.Request) {
url = resource.url;
// 合并Request本身headers + init传入headers
const reqClone = new unsafeWindow.Request(resource, init || {});
headersRaw = reqClone.headers;
return originalFetch(reqClone);
} else {
url = resource;
headersRaw = new unsafeWindow.Headers(init?.headers || {});
}
if (!API_FIX_HEADERS.Authorization) {
for (const [key, value] of Object.entries(API_FIX_HEADERS)) {
if (!value) {
API_FIX_HEADERS[key] = headersRaw.get(key);
}
headersRaw.set(key, value);
}
console.log('[油猴-聚水团批量加购-Authorization]', API_FIX_HEADERS);
}
// 调用原始fetch
return originalFetch.call(unsafeWindow, resource, init);
};
// 通用POST fetch封装,自动携带鉴权头
async function apiFetch(url, body, retry) {
const res = await originalFetch(url, {
method: "POST",
headers: API_FIX_HEADERS,
body: JSON.stringify(body)
});
const resJson = await res.json()
if (resJson.code === '1016' && !retry) {
await new Promise(r => setTimeout(r, 5000));
return apiFetch(url, body, true)
}
return resJson
}
// 输出日志
function log(txt, color = "#333") {
if (!logDom) return
logDom.innerHTML += `
${txt}
`;
logDom.scrollTop = logDom.scrollHeight;
};
// 1. 获取我的供应商列表
async function getSupplierList() {
const data = {
"sort": "cdp.APPLY_TIME desc, cdp.DAYS_ORDER_QTY desc, cdp.SHELVES_QTY desc",
"status": "2",
"supplierRefundable": false,
"pageNum": 1,
"pageSize": 99
};
const json = await apiFetch("https://sc.scm121.com/api/drp/mySupplier/queryMySupplier", data);
if (!json.success || !Array.isArray(json.data) || !json.data.length) {
log("⛔ 获取供应商失败", "#f00")
throw new Error(json.message || "获取供应商失败");
}
supplierList = json.data;
return supplierList;
}
// 2. 根据spuId查询SPU基础信息(获取coId、itemCode)
async function getSpuBySkuId(targetSkuId) {
const data = {
supplierId: selectedSupplierId,
spuHaveVideo: false,
sort: "spu.sorted desc,spu.UPDATED DESC,spu.ID ASC",
itemCodeVague: "0",
itemCodes: targetSkuId,
styleCodeVague: "0",
hasStock: false,
pageNum: 1,
pageSize: 99,
asyncFillField: true
};
const json = await apiFetch("https://sc.scm121.com/api/goods/buyer/supplier/goods/list", data);
return json?.data?.[0]
}
// 3. 获取 spu 下的 sku
async function getSkuList(itemCoId, itemSpuId) {
const data = { itemCoId, itemSpuId };
const json = await apiFetch("https://sc.scm121.com/api/goods/goods/querySkuInfoBySpuId", data);
const skuArr = json?.data?.itemSkuList || [];
// 只匹配输入的款码,返回单个SKU
return skuArr
}
// 4. 批量加入购物车接口
async function addCartBatch(cartArr) {
const res = await apiFetch("https://sc.scm121.com/api/goods/goods/channelCart/addCart", cartArr);
return res;
}
// 单行处理:单个skuId + 采购数量
async function handleSingleSku(skuId, num) {
try {
log(`[SKU] ${skuId},采购数量:${num}`);
// 查询该款码所属SPU基础数据
const spuInfo = await getSpuBySkuId(skuId);
if (!spuInfo) {
log(`❌ [SKU]${skuId} 在当前供应商下未找到商品,跳过`, "#f00");
return;
}
const { itemCoId, itemSpuId } = spuInfo;
// 只查询当前输入的这一个SKU,过滤其他尺码
const skuInfo = (await getSkuList(itemCoId, itemSpuId)).find(sku => sku.itemCode === skuId);
if (!skuInfo) {
log(`⚠️ [SKU]${skuId} 对应款码未找到,跳过`, "#f00");
return;
}
return {
"itemSkuId": skuInfo.id,
"itemSpuId": itemSpuId,
"itemCoId": itemCoId,
"addQty": num,
"itemCode": skuId
}
} catch (err) {
log(`❌ SKU ${skuId} 执行异常:${err.message}`, "#f00");
console.error(err);
}
}
// 解析文本,逐行执行
async function parseAndRun(textareaVal) {
if (!API_FIX_HEADERS.Authorization) {
alert("未捕获Authorization!请刷新页面并打开商品列表触发一次接口请求");
return;
}
logDom.innerHTML = "";
const cartArr = [];
const lines = textareaVal.trim().split(/\r?\n/);
for (const line of lines) {
const trimLine = line.trim();
if (!trimLine) continue;
const parts = trimLine.split(/\s+/);
if (parts.length < 2) {
log(`⚠️ 格式错误跳过该行:${line},正确格式:SKU 数量`, "#f60");
continue;
}
const skuId = parts[0];
const num = parseInt(parts[1], 10);
if (isNaN(num) || num <= 0) {
log(`⚠️ 数量非法跳过该行:${line}`, "#f60");
continue;
}
const cartItem = await handleSingleSku(skuId, num);
if (cartItem) {
cartArr.push(cartItem);
}
// 简单延时防接口限流
await new Promise(r => setTimeout(r, 2000));
}
if (cartArr.length) {
const res = await addCartBatch(cartArr);
if (res.success) {
log(`== 批量加入购物车成功 ==`, "#090");
for (const line of lines) {
const trimLine = line.trim();
if (!trimLine) continue;
const parts = trimLine.split(/\s+/);
const skuId = parts[0];
if (cartArr.some(item => item.itemCode === skuId)) {
log(line + ' ✅');
} else {
log(line + ' ❌');
}
}
} else {
log(`== 批量加入购物车失败 ==`, "#f00");
}
}
log("== 全部任务执行完毕 ==", "#009");
}
// 加载供应商
async function loadSuppliers() {
const selectDom = document.getElementById("supplierSelect");
selectDom.addEventListener("change", () => {
selectedSupplierId = selectDom.value;
GM_setValue('last_selected_supplier_id', selectedSupplierId);
});
supplierList = await getSupplierList()
selectDom.innerHTML = '';
supplierList.forEach(sup => {
const opt = document.createElement("option");
opt.value = sup.supplierId;
opt.textContent = `${sup.coName}`;
selectDom.appendChild(opt);
});
let lastSelectedSupplierId = GM_getValue('last_selected_supplier_id', supplierList[0].supplierId);
if (supplierList.some(sup => sup.supplierId === lastSelectedSupplierId)) {
selectedSupplierId = lastSelectedSupplierId;
} else {
selectedSupplierId = supplierList[0].supplierId;
}
selectDom.value = selectedSupplierId
}
// 创建悬浮面板(仅菜单点击时调用)
function createShopcarPanel() {
const stockDom = document.getElementById(STOCK_PANEL_ID)
if (stockDom) {
document.body.removeChild(stockDom);
}
// 已存在面板直接返回,避免重复生成
if (document.getElementById(PANEL_ID)) return;
const wrap = document.createElement("div");
wrap.id = PANEL_ID;
wrap.style = `
position: fixed;top:80px;right:16px;z-index:999999;
width:360px;background:#fff;border:1px solid #ddd;border-radius:8px;
padding:14px;box-shadow:0 2px 14px rgba(0,0,0,0.15);
`;
wrap.innerHTML = `
批量采购
提示:登录后刷新页面自动抓取鉴权Token (版本:${scriptVersion})
`;
document.body.appendChild(wrap);
const textarea = document.getElementById("spuTextarea");
const runBtn = document.getElementById("startRunBtn");
const closeBtn = document.getElementById("closePanelBtn");
logDom = document.getElementById("logBox");
loadSuppliers()
runBtn.addEventListener("click", () => {
if (!selectedSupplierId) {
log('⚠️ 等待加载供应商')
} else {
parseAndRun(textarea.value);
}
});
// 关闭面板按钮
closeBtn.addEventListener("click", () => {
wrap.remove()
logDom = undefined
selectedSupplierId = undefined
});
}
// 注册油猴菜单:点击油猴图标 → 本脚本 → 打开批量加购面板
GM_registerMenuCommand("打开批量采购面板", createShopcarPanel, "b");
const STOCK_PANEL_ID = 'jushuituan_stock_panel'
// ============================================================
// 1. 核心处理函数(你可以修改这里的逻辑来定制处理方式)
// ============================================================
async function processExcelData(dataArray) {
// 示例处理:为每一行添加 3 个新字段
// 你可以改成任何你需要的转换逻辑(过滤、字段映射、计算等)
let lastSkuList = []
for (const [index, row] of dataArray.entries()) {
const skuId = row['SKU货号(选填)']
let skuInfo = lastSkuList.find(sku => sku.itemCode === skuId)
if (!skuInfo) {
await new Promise(r => setTimeout(r, 2000));
const spuInfo = await getSpuBySkuId(skuId);
if (!spuInfo) continue
await new Promise(r => setTimeout(r, 300));
const { itemCoId, itemSpuId } = spuInfo;
lastSkuList.push(...(await getSkuList(itemCoId, itemSpuId)))
skuInfo = lastSkuList.find(sku => sku.itemCode === skuId)
}
if (!skuInfo) continue
if ( row['当前库存'] == -1) {
row['修改后库存'] = -1
} if (skuInfo.stock < 5) {
row['修改后库存'] = 0
} else {
row['修改后库存'] = Math.floor(skuInfo.stock * 0.8)
}
log(`进度 ${index + 1} / ${dataArray.length}`)
}
return dataArray
}
async function onExcelUpload(file) {
log(`正在解析Excel 。`);
// ===== 确保 XLSX 可用 =====
const XLSX = window.XLSX || unsafeWindow.XLSX;
if (!XLSX) {
log(`❌ XLSX 库未加载,请检查网络或刷新页面重试。`, "#f00");
return
}
const reader = new FileReader();
reader.onload = async function (ev) {
try {
const data = new Uint8Array(ev.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(firstSheet);
if (!jsonData || jsonData.length === 0) {
log(`❌ Excel 为空或解析失败。`, "#f00");
return;
}
const processedData = await processExcelData(jsonData);
console.log(processedData)
const newSheet = XLSX.utils.json_to_sheet(processedData);
const newWorkbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(newWorkbook, newSheet, '库存设置');
const wbout = XLSX.write(newWorkbook, {
bookType: 'xlsx',
type: 'array'
});
const blob = new Blob([wbout], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const downloadName = `库存${getTodayMMDD()}.xlsx`;
// ----- 使用 a 标签下载(不依赖 GM_download)-----
const link = document.createElement('a');
link.href = url;
link.download = downloadName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setTimeout(() => URL.revokeObjectURL(url), 5000);
log(`✅ 正在下载Excel 。`);
} catch (err) {
console.error('❌ Excel 处理出错');
console.error(err)
log(`❌ Excel 处理出错。`, "#f00");
}
};
reader.onerror = function () {
alert('❌ 读取文件失败');
log(`❌ Excel 读取文件失败。`, "#f00");
};
reader.readAsArrayBuffer(file);
};
// ============================================================
// 2. 核心流程:上传 → 解析 → 处理 → 自动下载
// ============================================================
function createStockPanel() {
// 已存在面板直接返回,避免重复生成
const carDom = document.getElementById(PANEL_ID)
if (carDom) {
document.body.removeChild(carDom);
}
if (document.getElementById(STOCK_PANEL_ID)) return;
const wrap = document.createElement("div");
wrap.id = STOCK_PANEL_ID;
wrap.style = `
position: fixed;top:80px;right:16px;z-index:999999;
width:360px;background:#fff;border:1px solid #ddd;border-radius:8px;
padding:14px;box-shadow:0 2px 14px rgba(0,0,0,0.15);
`;
wrap.innerHTML = `
批量库存查询
📂 文件上传
📁
点击选择 或 拖拽文件到这里
支持 .xlsx / .xls / .txt / .csv
提示:登录后刷新页面自动抓取鉴权Token (版本:${scriptVersion})
`;
document.body.appendChild(wrap);
const runBtn = document.getElementById("startRunBtn");
const closeBtn = document.getElementById("closePanelBtn");
// ==================== 获取元素引用 ====================
const dropArea = document.getElementById('dropArea');
const fileInfo = document.getElementById('fileInfo');
const processBtn = document.getElementById('processBtn');
const statusMsg = document.getElementById('statusMsg');
// 隐藏的文件输入(也在这里创建)
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.xlsx,.xls,.txt,.csv';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
logDom = document.getElementById("logBox");
loadSuppliers()
// ==================== 事件绑定 ====================
// 点击拖拽区域触发文件选择
dropArea.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', function () {
if (this.files && this.files.length > 0) {
const file = this.files[0];
if (file) onExcelUpload(file)
}
this.value = '';
});
// 拖拽事件
dropArea.addEventListener('dragover', (e) => {
e.preventDefault();
dropArea.style.borderColor = '#4A90D9';
dropArea.style.background = '#f0f7ff';
});
dropArea.addEventListener('dragleave', (e) => {
e.preventDefault();
dropArea.style.borderColor = '#ccc';
dropArea.style.background = 'transparent';
});
dropArea.addEventListener('drop', (e) => {
e.preventDefault();
dropArea.style.borderColor = '#ccc';
dropArea.style.background = 'transparent';
const files = e.dataTransfer.files;
if (files && files.length > 0) {
const file = files[0];
if (file) onExcelUpload(file)
}
});
// 关闭面板按钮
closeBtn.addEventListener("click", () => {
wrap.remove()
document.body.removeChild(fileInput);
logDom = undefined
selectedSupplierId = undefined
});
}
function getTodayMMDD() {
const now = new Date();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return month + day;
}
// ============================================================
// 3. 注册 Tampermonkey 菜单命令
// ============================================================
GM_registerMenuCommand('打开获取库存面板', createStockPanel);
})();