// ==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 = `

批量采购

格式:每行 sku 采购数量

执行日志:

提示:登录后刷新页面自动抓取鉴权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); })();