// ==UserScript==
// @name 待出库自动备注虚拟手机号
// @namespace https://porder.shop.jd.com/
// @version 3.4
// @description 对于京东手机号脱敏的解决方式之一 - 性能优化版
// @author YOU
// @license YOU
// @match *
// @match https://shop.jd.com/jdm/trade/orders/order-list?tabType=waitOut
// @icon https://www.jd.com/favicon.ico
// @require https://update.greasyfork.org/scripts/508394/1447419/crypto.js
// @require https://update.greasyfork.org/scripts/448906/1077566/htmp.js
// @grant none
// ==/UserScript==
// 全局配置
const CONFIG = {
BATCH_SIZE: 20, // 批量处理订单数量
RETRY_TIMES: 3, // API调用重试次数
RETRY_DELAY: 1000, // 重试延迟(ms)
API_DELAY: 500, // API调用间隔(ms)
AUTO_REFRESH: 60000, // 自动刷新间隔(ms)
MAX_CONCURRENT: 5 // 最大并发请求数
};
// 正则表达式
const REGEX = {
真实手机号: /[0-9]{12}/g,
虚拟手机号: /[0-9]{12}-[0-9]{4}/g
};
// 状态管理
const State = {
running: false,
订单总量: 0,
处理计数: 0,
失败计数: 0,
备注真实号码: false,
备注虚拟号码: false,
循环执行备注手机号: null
};
var AccountInfo;
// API请求限流器
class RateLimiter {
constructor(maxConcurrent) {
this.queue = [];
this.running = 0;
this.maxConcurrent = maxConcurrent;
}
async add(fn) {
if (this.running >= this.maxConcurrent) {
await new Promise(resolve => this.queue.push(resolve));
}
this.running++;
try {
return await fn();
} finally {
this.running--;
if (this.queue.length > 0) {
const next = this.queue.shift();
next();
}
}
}
}
const rateLimiter = new RateLimiter(CONFIG.MAX_CONCURRENT);
// 工具函数
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const retry = async (fn, times = CONFIG.RETRY_TIMES) => {
for (let i = 0; i < times; i++) {
try {
return await fn();
} catch (err) {
if (i === times - 1) throw err;
await sleep(CONFIG.RETRY_DELAY);
}
}
};
// UI更新函数
function updateUI() {
const button = document.getElementById("yijiansjh");
if (!button) return;
button.innerHTML = State.running ?
`停止备注 (${State.处理计数}/${State.订单总量}, 失败: ${State.失败计数})` :
"开始自动备注客户手机号";
}
// 批量处理订单
async function processBatch(orders) {
// 首先批量获取所有订单的备注信息
const orderIds = orders.map(order => order.orderId);
const remarkInfos = await retry(() => 批量获取订单备注(orderIds));
// 将备注信息转换为Map以便快速查找
const remarkMap = new Map(
remarkInfos.map(info => [info.orderId, info.remark || ""])
);
const tasks = orders.map(order => {
return rateLimiter.add(async () => {
try {
await processOrder(order, remarkMap.get(order.orderId));
State.处理计数++;
} catch (err) {
console.error(`处理订单失败 ${order.orderId}:`, err);
State.失败计数++;
} finally {
updateUI();
}
});
});
await Promise.all(tasks);
}
// 处理单个订单
async function processOrder(order, currentRemark = "") {
const { orderId, userPin } = order;
let newRemark = currentRemark;
let needsUpdate = false;
// 检查虚拟号
if (State.备注虚拟号码) {
const virtualPhone = await retry(() => 获取虚拟手机号(userPin, orderId));
if (virtualPhone) {
const virtualTag = `${virtualPhone}`;
// 检查是否已经包含这个虚拟号
if (!currentRemark.includes(virtualTag)) {
newRemark += ` ${virtualTag}`;
needsUpdate = true;
}
}
}
// 检查真实号
if (State.备注真实号码) {
const realPhone = await retry(() => 获取真实手机号(userPin, orderId));
if (realPhone) {
const realTag = `${realPhone}`;
// 检查是否已经包含这个真实号
if (!currentRemark.includes(realTag)) {
newRemark += ` ${realTag}`;
needsUpdate = true;
}
}
}
if (needsUpdate) {
// 清理可能的多余空格
newRemark = newRemark.trim().replace(/\s+/g, ' ');
await retry(() => 备注手机号(orderId, newRemark));
}
}
// 批量获取订单备注信息
async function 批量获取订单备注(orderIds) {
const url = "https://sff.jd.com/api?v=1.0&appId=COCX0HBWR4BA7RDVDBIQ&api=dsm.order.manage.orderlist.getVenderRemarkList";
const bodyData = {
orderIds: orderIds,
accessContext: { source: "web" }
};
const response = await fetch(url, {
method: "POST",
headers: {
"accept": "application/json, text/plain, */*",
"content-type": "application/json;charset=UTF-8",
"dsm-platform": "pc"
},
body: JSON.stringify(bodyData),
credentials: "include"
});
if (!response.ok) {
throw new Error('批量获取备注信息失败');
}
const data = await response.json();
return data.data || [];
}
// 主函数优化
async function main() {
if (!State.running) return;
try {
let page = 1;
const pageSize = CONFIG.BATCH_SIZE;
State.处理计数 = 0;
State.失败计数 = 0;
do {
const response = await retry(() => 获取订单列表(page, pageSize));
if (!response?.data?.results) break;
const orders = response.data.results;
State.订单总量 = response.data.total;
await processBatch(orders);
await sleep(CONFIG.API_DELAY);
page++;
} while (page <= Math.ceil(State.订单总量 / pageSize) && State.running);
} catch (err) {
console.error('执行失败:', err);
State.running = false;
} finally {
updateUI();
}
}
// 初始化函数
function init() {
// 添加UI元素
addbut();
// 定时检查页面URL
setInterval(() => {
const matchesUrl = window.location.href.includes('waitOut');
if (matchesUrl && !document.getElementById("yijiansjh")) {
addbut();
}
}, 1000);
}
// 启动脚本
init();
function addbut() {
// 创建样式
const style = document.createElement('style');
style.textContent = `
.batch-operate-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.custom-container {
display: inline-flex;
align-items: center;
gap: 10px;
margin-left: 8px;
padding-left: 8px;
position: relative;
}
.custom-container::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
height: 16px;
width: 1px;
background-color: #dcdee2;
}
.custom-button {
background: #fff;
border: 1px solid #3768fa;
color: #3768fa;
border-radius: 4px;
height: 32px;
padding: 0 15px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
min-width: 200px;
}
.custom-button:hover {
background: #3768fa;
color: #fff;
}
.custom-button.running {
background: #ff4d4f;
border-color: #ff4d4f;
color: #fff;
}
.custom-checkbox-wrapper {
display: inline-flex;
align-items: center;
cursor: pointer;
}
.custom-checkbox {
appearance: none;
width: 16px;
height: 16px;
border: 1px solid #dcdee2;
border-radius: 2px;
margin-right: 6px;
position: relative;
cursor: pointer;
transition: all 0.2s;
}
.custom-checkbox:checked {
background: #3768fa;
border-color: #3768fa;
}
.custom-checkbox:checked::after {
content: '';
position: absolute;
left: 4px;
top: 1px;
width: 6px;
height: 9px;
border: 2px solid #fff;
border-top: 0;
border-left: 0;
transform: rotate(45deg);
}
.custom-label {
color: #515a6e;
font-size: 14px;
user-select: none;
}
.progress-info {
font-size: 12px;
color: #666;
margin-left: 10px;
}
`;
document.head.appendChild(style);
// 创建容器
const container = document.createElement("div");
container.className = "custom-container";
// 创建按钮
const button = document.createElement("button");
button.id = "yijiansjh";
button.className = "custom-button";
button.innerHTML = "开始自动备注客户手机号";
// 创建真实号选择框
const realNumberWrapper = document.createElement("label");
realNumberWrapper.className = "custom-checkbox-wrapper";
const realNumberCheckbox = document.createElement("input");
realNumberCheckbox.type = "checkbox";
realNumberCheckbox.id = "realNumber";
realNumberCheckbox.className = "custom-checkbox";
const realNumberLabel = document.createElement("span");
realNumberLabel.className = "custom-label";
realNumberLabel.innerText = "备注真实号";
// 创建虚拟号选择框
const virtualNumberWrapper = document.createElement("label");
virtualNumberWrapper.className = "custom-checkbox-wrapper";
const virtualNumberCheckbox = document.createElement("input");
virtualNumberCheckbox.type = "checkbox";
virtualNumberCheckbox.id = "virtualNumber";
virtualNumberCheckbox.className = "custom-checkbox";
const virtualNumberLabel = document.createElement("span");
virtualNumberLabel.className = "custom-label";
virtualNumberLabel.innerText = "备注虚拟号";
// 创建进度信息
const progressInfo = document.createElement("span");
progressInfo.className = "progress-info";
progressInfo.id = "progressInfo";
// 组装DOM
realNumberWrapper.appendChild(realNumberCheckbox);
realNumberWrapper.appendChild(realNumberLabel);
virtualNumberWrapper.appendChild(virtualNumberCheckbox);
virtualNumberWrapper.appendChild(virtualNumberLabel);
container.appendChild(button);
container.appendChild(realNumberWrapper);
container.appendChild(virtualNumberWrapper);
container.appendChild(progressInfo);
// 添加到页面
const targetElement = document.getElementsByClassName("batch-operate-buttons")[0];
if (targetElement) {
targetElement.appendChild(container);
}
// 添加按钮点击事件
button.onclick = async function () {
if (!State.running) {
if (!realNumberCheckbox.checked && !virtualNumberCheckbox.checked) {
alert('请至少选择一种备注方式(真实号或虚拟号)');
return;
}
State.running = true;
State.备注真实号码 = realNumberCheckbox.checked;
State.备注虚拟号码 = virtualNumberCheckbox.checked;
button.classList.add('running');
// 禁用复选框
realNumberCheckbox.disabled = true;
virtualNumberCheckbox.disabled = true;
AccountInfo = await GetAccountInfo();//获取店铺信息
main();
State.循环执行备注手机号 = setInterval(main, CONFIG.AUTO_REFRESH);
} else {
State.running = false;
button.classList.remove('running');
// 启用复选框
realNumberCheckbox.disabled = false;
virtualNumberCheckbox.disabled = false;
if (State.循环执行备注手机号) {
clearInterval(State.循环执行备注手机号);
State.循环执行备注手机号 = null;
}
}
updateUI();
};
}
// API函数
async function 获取订单列表(页码, 获取数量) {
const url = "/api?v=1.0&appId=COCX0HBWR4BA7RDVDBIQ&api=dsm.order.manage.orderlist.queryOrderPage";
const obj = {
"request": {
"source": "2000",
"versionNo": 20240830,
"data": {
"consumerName": null,
"consumerMobilePhone": null,
"logiNo": null,
"venderRemarkLevels": [],
"orderCreateDateRange": getDateRange(),
"remarkFlag": null,
"userPin": null,
"itemNum": null,
"storeId": null,
"orderTags": [],
"orderStatusTypes": [
"2"
],
"idSopShipmentType": null,
"paymentType": null,
"orderCompleteDateRange": null,
"orderSalType": null,
"orderBusinessType": null,
"ouId": null,
"purchaseOrderNo": null,
"firstOrderId": null,
"tradeVendorId": null,
"supplierId": null,
"orderId": null,
"orderIds": null,
"orderTab": "waitOut",
"sortMode": "orderDateAsc",
"current": 页码,
"pageSize": 获取数量,
"monthsFlag": "within6months"
}
},
"accessContext": {
"source": "web"
}
};
const baseUrl = "https://sff.jd.com";
const signAppId = "0248a";
const colorParamSign = buildColorParamSign(url, obj);
const response = await fetch(baseUrl + url, {
method: 'POST',
headers: {
'Content-type': 'application/json;charset=UTF-8',
'dsm-platform': 'pc',
"h5st": await get_h5st(colorParamSign, signAppId)
},
body: JSON.stringify(obj),
credentials: 'include'
});
if (!response.ok) {
throw new Error('获取订单列表失败');
}
return await response.json();
}
async function GetAccountInfo(){
const url = "/api?v=1.0&appId=ZSCUMIUH2ZNF8Z2PVU1J&api=dsm.shop.pageframe.navigation.accountFacade.findAccountInfo";
const obj = {
"body": "44136FA355B3678A1146AD16F7E8649E94FB4FC21FE77E8310C060F61CAAFF8A",
"appId": "ZSCUMIUH2ZNF8Z2PVU1J",
"api": "dsm.shop.pageframe.navigation.accountFacade.findAccountInfo",
"v": "1.0"
};
const baseUrl = "https://sff.jd.com";
const signAppId = "0248a";
const colorParamSign = buildColorParamSign(url, obj);
const response = await fetch(baseUrl + url, {
method: 'POST',
headers: {
'Content-type': 'application/json;charset=UTF-8',
'dsm-platform': 'pc',
"h5st": await get_h5st(colorParamSign, signAppId)
},
body: "{}",
credentials: 'include'
});
if (!response.ok) {
throw new Error('获取店铺信息失败');
}
return await response.json();
}
async function 获取虚拟手机号(userPin, orderId) {
const url = "https://api.m.jd.com/client.action";
const signAppId = "62ed1";
var obj = JSON.stringify({
skuId: "0",
customer: encryptPin(userPin),
orderId: orderId,
params: {
"appid": "im.customer",
"BPOPmpin": "",
"BPOPvid": "",
"BPOPcustomer": "",
"BPOPstoreId": ""
},
encryptNew: "true",
authType: 4,
ddAuthUUid: {
"appId": "im.waiter",
"pin": AccountInfo.data.pin,
"aid": "jm_OcZ0hBbI",
"client": "pc",
"venderId": AccountInfo.data.venderId
},
customerInfo: {
"appId": "im.customer"
}
})
const colorParamSign = {
functionId: "halleyOrderDetail",
body: Sha256ToStr(obj),
appid: "customerAssistant",
clientVersion: "normal",
lang: "zh_CN"
};
const params = {
appid: "customerAssistant",
functionId: "halleyOrderDetail",
body: obj,
h5st: await get_h5st(colorParamSign, signAppId),
clientVersion: "20230720",
client: "assistant_web",
lang: "zh_CN"
};
const queryString = Object.entries(params)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join("&");
const fullUrl = `${url}?${queryString}`;
const response = await fetch(fullUrl, {
credentials: "include"
});
if (!response.ok) {
throw new Error('获取虚拟手机号失败');
}
const data = await response.json();
let tel = data.data.tel;
if (tel.includes('*')) {
const addressKey = data.data.addressEncryptKey;
const customerKey = data.data.customerEncryptKey;
const telKey = data.data.telEncryptKey;
const obj = await 解密虚拟手机号(addressKey, customerKey, telKey, orderId);
tel = obj.data.secretNo;
}
return tel;
}
async function 获取真实手机号(userPin, orderId) {
const url = "https://api.m.jd.com/client.action";
const signAppId = "62ed1";
var obj = JSON.stringify({
skuId: "0",
customer: encryptPin(userPin),
orderId: orderId,
params: {
"appid": "im.customer",
"BPOPmpin": "",
"BPOPvid": "",
"BPOPcustomer": "",
"BPOPstoreId": ""
},
encryptNew: "true",
authType: 4,
ddAuthUUid: {
"appId": "im.waiter",
"pin": AccountInfo.data.pin,
"aid": "jm_OcZ0hBbI",
"client": "pc",
"venderId": AccountInfo.data.venderId
},
customerInfo: {
"appId": "im.customer"
}
})
const colorParamSign = {
functionId: "halleyOrderDetail",
body: Sha256ToStr(obj),
appid: "customerAssistant",
clientVersion: "normal",
lang: "zh_CN"
};
const params = {
appid: "customerAssistant",
functionId: "halleyOrderDetail",
body: obj,
h5st: await get_h5st(colorParamSign, signAppId),
clientVersion: "20230720",
client: "assistant_web",
lang: "zh_CN"
};
const queryString = Object.entries(params)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join("&");
const fullUrl = `${url}?${queryString}`;
const response = await fetch(fullUrl, {
credentials: "include"
});
if (!response.ok) {
throw new Error('获取虚拟手机号失败');
}
const data = await response.json();
let tel = data.data.tel;
if (tel.includes('*')) {
const addressKey = data.data.addressEncryptKey;
const customerKey = data.data.customerEncryptKey;
const telKey = data.data.telEncryptKey;
const obj = await 解密虚拟手机号(addressKey, customerKey, telKey, orderId);
tel = obj.data.secretNo;
}
return tel;
}
async function 获取真实手机号(userPin, orderId) {
const url = "https://api.m.jd.com/client.action";
const params = {
functionId: "halleyOrderDetail",
body: JSON.stringify({
skuId: "0",
customer: encryptPin(userPin),
orderId: orderId,
encryptNew: "true",
authType: 4
}),
appid: "customerAssistant",
clientVersion: "normal",
lang: "zh_CN"
};
const queryString = Object.entries(params)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join("&");
const fullUrl = `${url}?${queryString}`;
const response = await fetch(fullUrl, {
credentials: "include"
});
if (!response.ok) {
throw new Error('获取真实手机号失败');
}
const data = await response.json();
let tel = data.data.tel;
if (tel.includes('*')) {
const addressKey = data.data.addressEncryptKey;
const customerKey = data.data.customerEncryptKey;
const telKey = data.data.telEncryptKey;
const obj = await 解密真实手机号(addressKey, customerKey, telKey);
tel = obj.data[telKey];
}
return tel;
}
async function 解密虚拟手机号(addressKey, customerKey, telKey, orderId) {
const url = "https://api.m.jd.com/client.action";
const params = {
functionId: "decryptBindPhone",
body: JSON.stringify({
sceneType: "forward",
expiration: 30,
orderId: orderId,
phoneNoEncryptKey: telKey,
chargingId: orderId,
params: {
"appid": "im.customer",
"BPOPmpin": "",
"BPOPvid": "",
"BPOPcustomer": "",
"BPOPstoreId": ""
},
encryptNew: "true",
authType: 4
}),
appid: "customerAssistant",
clientVersion: 20230720,
client: "assistant_web",
lang: "zh_CN"
};
const queryString = Object.entries(params)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join("&");
const fullUrl = `${url}?${queryString}`;
const response = await fetch(fullUrl, {
credentials: "include"
});
if (!response.ok) {
throw new Error('解密虚拟手机号失败');
}
return await response.json();
}
async function 解密真实手机号(addressKey, customerKey, telKey) {
const url = "https://api.m.jd.com/client.action";
const params = {
functionId: "batchDecrypt",
body: JSON.stringify({
params: {
appid: "im.customer",
BPOPmpin: "",
BPOPvid: "",
BPOPcustomer: "",
BPOPstoreId: ""
},
encryKeys: [
addressKey,
customerKey,
telKey
],
encryptNew: "true",
authType: 4
}),
appid: "customerAssistant",
clientVersion: "normal",
lang: "zh_CN"
};
const queryString = Object.entries(params)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join("&");
const fullUrl = `${url}?${queryString}`;
const response = await fetch(fullUrl, {
credentials: "include"
});
if (!response.ok) {
throw new Error('解密真实手机号失败');
}
return await response.json();
}
async function 备注手机号(订单号, 备注内容) {
const url = "/api?v=1.0&appId=COCX0HBWR4BA7RDVDBIQ&api=dsm.order.manage.orderlist.batchSubmitVenderRemark";
const obj = {
param: {
level: 0,
levelDesc: "",
remark: 备注内容,
orderIds: [订单号],
accessContext: { source: "web" }
}
};
const baseUrl = "https://sff.jd.com";
const signAppId = "0248a";
const colorParamSign = buildColorParamSign(url, obj);
const response = await fetch(baseUrl + url, {
method: 'POST',
headers: {
'accept': 'application/json, text/plain, */*',
'Content-type': 'application/json;charset=UTF-8',
'dsm-platform': 'pc',
"h5st": await get_h5st(colorParamSign, signAppId)
},
body: JSON.stringify(obj),
credentials: 'include'
});
if (!response.ok) {
throw new Error('备注手机号失败');
}
return await response.json();
}
// 辅助函数
function getDateRange() {
const endDate = new Date();
const startDate = new Date(endDate);
startDate.setMonth(startDate.getMonth() - 6);
const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
return [
`${formatDate(startDate)} 00:00:00`,
`${formatDate(endDate)} 23:59:59`
];
}
function encryptPin(e) {
const key = CryptoJS.enc.Utf8.parse("#1*x3zp_");
const blockSize = 8;
let result = '';
for (let i = 0; i < e.length; i += blockSize) {
const block = e.substr(i, blockSize);
const encryptedBlock = CryptoJS.DES.encrypt(block, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.ZeroPadding
}).ciphertext.toString();
result += encryptedBlock.toUpperCase();
}
const encrypted = CryptoJS.enc.Hex.stringify(CryptoJS.enc.Utf8.parse(result));
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Hex.parse(encrypted));
}
function Sha256ToStr(e) {
const context = window.__MICRO_APP_PROXY_WINDOW__ || window;
return context.CryptoJS.SHA256(JSON.stringify(e)).toString().toUpperCase();
}
function parseSearchParams(e) {
void 0 === e && (e = "");
const t = e.split("?")[1] || e;
return t.split("&").reduce((e, t) => {
const [r, o] = t.split("=").map(decodeURIComponent);
e[r] = o;
return e;
}, {});
}
function buildColorParamSign(url, data) {
const s = Sha256ToStr(data);
const c = parseSearchParams(url);
const { v, appId, api } = c;
return {
body: s,
appId,
api,
v
};
}
function initPSign(appId, options = {}) {
if (!appId) {
throw new Error('appId is required');
}
const defaultOptions = {
preRequest: false,
debug: false,
onSign: function(e) {}
};
const context = window.__MICRO_APP_PROXY_WINDOW__ || window;
return new context.ParamsSign({
appId,
...defaultOptions,
...options
});
}
async function get_h5st(colorParamSign, signAppId) {
try {
const pSign = initPSign(signAppId);
const signedParams = await pSign.sign(colorParamSign);
return encodeURI(signedParams.h5st);
} catch (error) {
console.error('获取h5st失败:', error);
throw error;
}
}