// ==UserScript==
// @name 自动填写身份证号码
// @namespace http://tampermonkey.net/
// @version 0.3
// @description 使用前请添加乘客的姓名和随机的身份证号,脚本会自动生成所有可能的身份证号码,并自动填写到12306的乘客信息页面上用以验算身份证信息是否正确,我愿称之为绝配小工具!
// @author chiupam
// @icon https://kyfw.12306.cn/otn/images/favicon.ico
// @match https://kyfw.12306.cn/otn/passengers/init
// @match https://kyfw.12306.cn/otn/view/passengers.html
// @match https://kyfw.12306.cn/otn/view/passenger_edit.html*
// @grant GM_setValue
// @grant GM_getValue
// @license GNU GPLv3
// ==/UserScript==
(function () {
'use strict';
// ===== 用户配置区域(使用前请修改) =====
// 12306 乘客信息页面的姓名
const username = ""; // 必填:请填写乘客姓名,例如:"张三"
// 12306 乘客信息页面的身份证号(部分已知的信息)
// 格式1:月日未知 - 例如:"1101011990xxxx3319"(中间4位用x表示)
// 格式2:序列码未知 - 例如:"110101199001018xxxx" (已知前14位,最后4位未知)
// 格式3:序列码未知但知道性别 - 例如:"11010119900821xx1x"(男性) "11010119900821xx2x"(女性)
const knowID = ""; // 必填:请填写部分已知的身份证号
// 自动操作延迟时间(毫秒)
const SAVE_DELAY = 5000; // 填写后等待点击保存的时间
const CONFIRM_DELAY = 5000; // 等待点击确认按钮的时间
const CHECK_DELAY = 5000; // 检查状态的间隔时间
// ===== 核心逻辑代码 =====
// 检查用户配置
if (!username || !knowID) {
alert('请先在脚本中配置乘客姓名和部分已知的身份证号!');
console.error('配置错误:乘客姓名和身份证号不能为空');
return;
}
// 开始执行脚本
let { func, args } = chooseType(knowID);
const ID_LIST = func(...args);
// 如果没有生成有效的身份证号,提示用户检查输入
if (!ID_LIST || ID_LIST.length === 0) {
alert('未能生成有效的身份证号码,请检查输入格式!');
console.error('生成身份证号失败:请检查输入格式');
return;
}
console.log(`已生成 ${ID_LIST.length} 个可能的身份证号码`);
/**
* 计算身份证校验码
* @param {string} id17 - 身份证前17位
* @returns {string} 校验码
*/
function calculateCheckDigit(id17) {
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
let total = 0;
for (let i = 0; i < 17; i++) {
total += parseInt(id17[i], 10) * weights[i];
}
const remainder = total % 11;
return checkCodes[remainder];
}
/**
* 判断是否为闰年
* @param {number} year - 年份
* @returns {boolean} 是否为闰年
*/
function isLeapYear(year) {
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}
/**
* 生成月日未知的身份证号列表
* @param {string} addressCode - 地址码
* @param {string} birthYear - 出生年
* @param {string} sequenceCode - 顺序码
* @param {string} genderCode - 性别码
* @param {string} targetCheck - 目标校验码
* @param {string} outputMode - 输出模式
* @returns {Array|void} 身份证号列表或控制台输出
*/
function idMonthDay(addressCode, birthYear, sequenceCode, genderCode, targetCheck, outputMode = 'list') {
const validIds = [];
const leap = isLeapYear(parseInt(birthYear, 10));
for (let month = 1; month <= 12; month++) {
let maxDay;
if ([4, 6, 9, 11].includes(month)) {
maxDay = 30;
} else if (month === 2) {
maxDay = leap ? 29 : 28;
} else {
maxDay = 31;
}
for (let day = 1; day <= maxDay; day++) {
const monthStr = String(month).padStart(2, '0');
const dayStr = String(day).padStart(2, '0');
const id17 = `${addressCode}${birthYear}${monthStr}${dayStr}${sequenceCode}${genderCode}`;
const checkDigit = calculateCheckDigit(id17);
if (checkDigit === targetCheck) {
const fullId = id17 + checkDigit;
validIds.push(fullId);
if (outputMode === 'str') {
console.log(`月份: ${monthStr}, 日期: ${dayStr}, 完整号码: ${fullId}`);
}
}
}
}
if (outputMode === 'str') {
console.log(`共找到 ${validIds.length} 个校验码为 ${targetCheck} 的身份证号码。`);
} else if (outputMode === 'list') {
return validIds;
}
}
/**
* 生成序列码未知的身份证号列表
* @param {string} addressCode - 地址码
* @param {string} birthYear - 出生年
* @param {string} birthMonth - 出生月
* @param {string} birthDay - 出生日
* @param {string} genderCode - 性别码
* @param {string} outputMode - 输出模式
* @returns {Array|void} 身份证号列表或控制台输出
*/
function idFromPrefix(addressCode, birthYear, birthMonth, birthDay, genderCode, outputMode = 'list') {
const validIds = [];
const prefix = `${addressCode}${birthYear}${birthMonth}${birthDay}`;
let sequenceRange;
const genderNum = parseInt(genderCode, 10);
if (!isNaN(genderNum)) {
// 如果指定了性别码,只生成对应性别的号码(奇数为男,偶数为女)
sequenceRange = (genderNum % 2 === 1) ? [1, 3, 5, 7, 9] : [0, 2, 4, 6, 8];
} else {
// 如果未指定性别,生成所有可能的顺序码
sequenceRange = [...Array(10).keys()];
}
for (let area = 1; area < 100; area++) {
const areaCode = String(area).padStart(2, '0');
for (const i of sequenceRange) {
const id17 = `${prefix}${areaCode}${i}`;
const checkDigit = calculateCheckDigit(id17);
const fullId = id17 + checkDigit;
validIds.push(fullId);
if (outputMode === 'str') {
console.log(`月份: ${birthMonth}, 日期: ${birthDay}, 序列码:${areaCode},性别码:${i},校验码:${checkDigit}, 完整号码: ${fullId}`);
}
}
}
if (outputMode === 'str') {
console.log(`共找到 ${validIds.length} 个身份证号码。`);
} else if (outputMode === 'list') {
return validIds;
}
}
/**
* 根据输入的身份证号格式选择生成方法
* @param {string} idNumber - 部分已知的身份证号
* @param {string} outputMode - 输出模式
* @returns {Object} 函数和参数
*/
function chooseType(idNumber, outputMode = 'list') {
if (idNumber.slice(10, 14) === 'xxxx') {
// 处理月日未知的情况
const addressCode = idNumber.slice(0, 6);
const birthYear = idNumber.slice(6, 10);
const sequenceCode = idNumber.slice(14, 16);
const genderCode = idNumber[16];
const checkDigit = idNumber[17];
return {
func: idMonthDay,
args: [addressCode, birthYear, sequenceCode, genderCode, checkDigit, outputMode]
};
} else {
// 处理序列码未知的情况
const addressCode = idNumber.slice(0, 6);
const birthYear = idNumber.slice(6, 10);
const birthMonth = idNumber.slice(10, 12);
const birthDay = idNumber.slice(12, 14);
const genderCode = idNumber[16] || ''; // 性别码可能未知
return {
func: idFromPrefix,
args: [addressCode, birthYear, birthMonth, birthDay, genderCode, outputMode]
};
}
}
/**
* 创建日志显示面板
* @returns {HTMLElement} 日志面板DOM元素
*/
function createLogDisplay() {
// 如果已存在日志面板,直接返回
const existingLog = document.getElementById('scriptLog');
if (existingLog) return existingLog;
const logDiv = document.createElement('div');
logDiv.id = 'scriptLog';
logDiv.style.cssText = `
position: fixed;
top: 10px;
left: 10px;
width: 400px;
height: 600px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 10px;
border-radius: 5px;
font-size: 14px;
z-index: 9999;
display: flex;
flex-direction: column;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
`;
// 创建标题栏
const titleBar = document.createElement('div');
titleBar.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid rgba(255,255,255,0.3);
`;
const title = document.createElement('div');
title.textContent = '自动填写身份证号码 - 状态面板';
title.style.fontWeight = 'bold';
const closeButton = document.createElement('button');
closeButton.textContent = '隐藏';
closeButton.style.cssText = `
background: #333;
color: white;
border: none;
padding: 3px 8px;
border-radius: 3px;
cursor: pointer;
`;
closeButton.onclick = function() {
logDiv.style.height = '30px';
logDiv.style.width = '200px';
closeButton.textContent = '显示';
closeButton.onclick = function() {
logDiv.style.height = '600px';
logDiv.style.width = '400px';
closeButton.textContent = '隐藏';
closeButton.onclick = arguments.callee.caller;
};
};
titleBar.appendChild(title);
titleBar.appendChild(closeButton);
// 创建状态面板
const statusPanel = document.createElement('div');
statusPanel.id = 'statusPanel';
statusPanel.style.cssText = `
margin-bottom: 10px;
padding: 10px;
background: rgba(255, 255, 255, 0.1);
border-radius: 5px;
`;
const currentIndex = GM_getValue('currentIndex', 0);
const nextId = currentIndex < ID_LIST.length ? ID_LIST[currentIndex] : '已用完';
statusPanel.innerHTML = `
<div style="margin-bottom: 5px;">当前进度: ${currentIndex}/${ID_LIST.length}</div>
<div style="margin-bottom: 5px;">下一个要尝试的号码: ${nextId}</div>
<div style="margin-bottom: 5px;">剩余数量: ${Math.max(0, ID_LIST.length - currentIndex)}</div>
<div style="display: flex; justify-content: space-between;">
<button id="resetIndex" style="
background: #ff4444;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
">重置进度</button>
<button id="pauseButton" style="
background: #4444ff;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
">暂停</button>
</div>
`;
// 创建日志区域
const logArea = document.createElement('div');
logArea.id = 'logArea';
logArea.style.cssText = `
flex: 1;
overflow-y: auto;
background: rgba(0, 0, 0, 0.3);
padding: 10px;
border-radius: 5px;
`;
logDiv.appendChild(titleBar);
logDiv.appendChild(statusPanel);
logDiv.appendChild(logArea);
document.body.appendChild(logDiv);
// 添加按钮事件监听
setTimeout(() => {
const resetButton = document.getElementById('resetIndex');
const pauseButton = document.getElementById('pauseButton');
addResetButtonListener(resetButton);
// 添加暂停按钮功能
if (pauseButton) {
const isPaused = GM_getValue('isPaused', false);
pauseButton.textContent = isPaused ? '继续' : '暂停';
pauseButton.addEventListener('click', () => {
const currentPaused = GM_getValue('isPaused', false);
GM_setValue('isPaused', !currentPaused);
pauseButton.textContent = !currentPaused ? '继续' : '暂停';
showLog(!currentPaused ? '⏸️ 操作已暂停' : '▶️ 操作已继续');
});
}
}, 0);
return logDiv;
}
let hasLoggedReset = false;
/**
* 添加重置按钮事件监听
* @param {HTMLElement} resetButton - 重置按钮元素
*/
function addResetButtonListener(resetButton) {
if (resetButton) {
resetButton.addEventListener('click', () => {
if (confirm('确定要重置进度吗?这将从第一个身份证号码重新开始。')) {
if (!hasLoggedReset) {
GM_setValue('currentIndex', 0);
showLog('🔄 进度已重置');
updateStatus();
hasLoggedReset = true;
setTimeout(() => {
showLog('🔄 即将刷新页面...');
setTimeout(() => {
window.location.reload();
}, 1000);
}, 500);
}
}
});
}
}
/**
* 更新状态面板
*/
function updateStatus() {
const statusPanel = document.getElementById('statusPanel');
if (statusPanel) {
const currentIndex = GM_getValue('currentIndex', 0);
const nextId = currentIndex < ID_LIST.length ? ID_LIST[currentIndex] : '已用完';
const isPaused = GM_getValue('isPaused', false);
statusPanel.innerHTML = `
<div style="margin-bottom: 5px;">当前进度: ${currentIndex}/${ID_LIST.length}</div>
<div style="margin-bottom: 5px;">下一个要尝试的号码: ${nextId}</div>
<div style="margin-bottom: 5px;">剩余数量: ${Math.max(0, ID_LIST.length - currentIndex)}</div>
<div style="display: flex; justify-content: space-between;">
<button id="resetIndex" style="
background: #ff4444;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
">重置进度</button>
<button id="pauseButton" style="
background: #4444ff;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
">${isPaused ? '继续' : '暂停'}</button>
</div>
`;
const resetButton = document.getElementById('resetIndex');
const pauseButton = document.getElementById('pauseButton');
addResetButtonListener(resetButton);
// 添加暂停按钮功能
if (pauseButton) {
pauseButton.addEventListener('click', () => {
const currentPaused = GM_getValue('isPaused', false);
GM_setValue('isPaused', !currentPaused);
pauseButton.textContent = !currentPaused ? '继续' : '暂停';
showLog(!currentPaused ? '⏸️ 操作已暂停' : '▶️ 操作已继续');
updateStatus();
});
}
}
}
/**
* 显示日志消息
* @param {string} message - 日志消息
*/
function showLog(message) {
const logDiv = document.querySelector('#scriptLog') || createLogDisplay();
const logArea = document.querySelector('#logArea');
const logEntry = document.createElement('div');
logEntry.style.cssText = `
margin-bottom: 5px;
padding: 5px;
border-bottom: 1px solid rgba(255,255,255,0.2);
`;
logEntry.innerHTML = `${new Date().toLocaleTimeString()} - ${message}`;
logArea.insertBefore(logEntry, logArea.firstChild);
console.log(message);
updateStatus();
}
// 创建日志显示面板
createLogDisplay();
// 检查是否处于暂停状态
const isPaused = GM_getValue('isPaused', false);
if (isPaused) {
showLog('⏸️ 脚本当前处于暂停状态,点击"继续"按钮恢复操作');
}
// 根据页面URL执行不同的操作逻辑
if (window.location.href.includes('passenger_edit.html')) {
// 乘客编辑页面逻辑
handleEditPage();
} else {
// 乘客列表页面逻辑
handleListPage();
}
/**
* 处理乘客编辑页面
*/
function handleEditPage() {
showLog('进入编辑页面,准备自动填写身份证号码');
setTimeout(() => {
if (GM_getValue('isPaused', false)) {
showLog('⏸️ 操作已暂停,请点击"继续"按钮恢复');
return;
}
const cardCodeInput = document.getElementById('cardCode');
if (cardCodeInput) {
let currentIndex = GM_getValue('currentIndex', 0);
if (currentIndex >= ID_LIST.length) {
showLog('⚠️ 已尝试完所有身份证号码,停止执行');
return;
}
const currentId = ID_LIST[currentIndex];
showLog(`🔄 第${currentIndex + 1}/${ID_LIST.length}个: ${currentId}`);
cardCodeInput.value = currentId;
// 更新索引到下一个
GM_setValue('currentIndex', currentIndex + 1);
// 触发输入事件
const event = new Event('input', { bubbles: true });
cardCodeInput.dispatchEvent(event);
// 等待后点击保存按钮
showLog(`⏳ 等待${SAVE_DELAY/1000}秒后将点击保存按钮...`);
setTimeout(() => {
if (GM_getValue('isPaused', false)) {
showLog('⏸️ 操作已暂停,请点击"继续"按钮恢复');
return;
}
const saveButton = document.querySelector('.btn.btn-primary');
if (saveButton) {
showLog('👆 点击保存按钮');
saveButton.click();
showLog('👀 等待确认按钮出现...');
let buttonCheckAttempts = 0;
const maxAttempts = 50; // 最多检查50次
const checkOkButton = setInterval(() => {
if (GM_getValue('isPaused', false)) {
showLog('⏸️ 操作已暂停,请点击"继续"按钮恢复');
clearInterval(checkOkButton);
return;
}
buttonCheckAttempts++;
if (buttonCheckAttempts > maxAttempts) {
clearInterval(checkOkButton);
showLog('⚠️ 确认按钮检查超时,尝试返回列表页');
// 尝试返回列表页
const backButton = document.querySelector('.goback');
if (backButton) {
backButton.click();
} else {
// 如果没有返回按钮,尝试返回上一页
window.history.back();
}
return;
}
const okButton = document.querySelector('.btn.btn-primary.ok');
if (okButton) {
showLog(`⏳ 找到确认按钮,${CONFIRM_DELAY/1000}秒后点击...`);
setTimeout(() => {
if (GM_getValue('isPaused', false)) {
showLog('⏸️ 操作已暂停,请点击"继续"按钮恢复');
return;
}
showLog('👆 点击确认按钮');
okButton.click();
}, CONFIRM_DELAY);
clearInterval(checkOkButton);
}
}, 100);
} else {
showLog('⚠️ 未找到保存按钮');
}
}, SAVE_DELAY);
} else {
showLog('⚠️ 未找到身份证输入框');
}
}, 3000);
}
/**
* 处理乘客列表页面
*/
function handleListPage() {
const currentIndex = GM_getValue('currentIndex', 0);
if (currentIndex >= ID_LIST.length) {
showLog('⚠️ 所有号码已尝试完毕,停止检查');
return;
}
showLog('进入列表页面,等待检查状态...');
let hasLoggedError = false;
/**
* 检查乘客状态并点击编辑按钮
*/
function checkAndClick() {
if (GM_getValue('isPaused', false)) {
showLog('⏸️ 操作已暂停,请点击"继续"按钮恢复');
return;
}
// 检查页面是否正在加载
const loadingIndicator = document.querySelector('.loading-mask');
if (loadingIndicator && loadingIndicator.style.display !== 'none') {
showLog(`⏳ 页面加载中,${CHECK_DELAY/1000}秒后重试...`);
setTimeout(checkAndClick, CHECK_DELAY);
return;
}
// 查找匹配乘客姓名的元素
const nameElements = Array.from(document.querySelectorAll('.name-yichu')).filter(el =>
el.textContent.trim() === username
);
if (nameElements.length === 0) {
showLog(`❌ 未找到${username}的信息,请检查姓名是否正确`);
return;
}
nameElements.forEach(nameElement => {
const row = nameElement.closest('tr');
if (!row) return;
const statusBox = row.querySelector('.verification-status-box .verification-status-user');
if (!statusBox) {
if (!hasLoggedError) {
showLog(`⏳ 状态框未加载,${CHECK_DELAY/1000}秒后重试...`);
}
setTimeout(checkAndClick, CHECK_DELAY);
return;
}
// 检查是否为错误状态
if (statusBox.classList.contains('user-check-error')) {
if (!hasLoggedError) {
showLog(`🔍 发现错误状态,等待${CHECK_DELAY/1000}秒后确认...`);
hasLoggedError = true;
setTimeout(() => {
if (GM_getValue('isPaused', false)) {
showLog('⏸️ 操作已暂停,请点击"继续"按钮恢复');
hasLoggedError = false;
return;
}
if (statusBox.classList.contains('user-check-error')) {
const editButton = row.querySelector('.one-edit');
if (editButton) {
showLog('👆 点击编辑按钮');
editButton.click();
} else {
showLog('⚠️ 未找到编辑按钮');
}
}
hasLoggedError = false;
}, CHECK_DELAY);
}
} else {
// 状态正常,可能已找到正确的身份证号
if (!hasLoggedError) {
const currentIndex = GM_getValue('currentIndex', 0);
const lastId = currentIndex > 0 ? ID_LIST[currentIndex - 1] : '未知';
showLog(`✅ 状态正常,当前身份证号码: ${lastId}`);
// 创建提示成功的通知
const successNotice = document.createElement('div');
successNotice.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 128, 0, 0.9);
color: white;
padding: 20px;
border-radius: 10px;
font-size: 16px;
text-align: center;
z-index: 10000;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
`;
successNotice.innerHTML = `
<h3 style="margin-top: 0;">成功找到正确的身份证号码</h3>
<p>乘客: ${username}</p>
<p>身份证号: ${lastId}</p>
<button id="closeSuccess" style="
background: white;
color: green;
border: none;
padding: 5px 15px;
border-radius: 5px;
margin-top: 10px;
cursor: pointer;
font-weight: bold;
">确定</button>
`;
document.body.appendChild(successNotice);
document.getElementById('closeSuccess').addEventListener('click', () => {
successNotice.remove();
});
hasLoggedError = true;
}
}
});
}
// 首次检查延迟
setTimeout(checkAndClick, 3000);
// 设置页面变化监听
const observer = new MutationObserver((mutations) => {
if (currentIndex < ID_LIST.length && !GM_getValue('isPaused', false)) {
setTimeout(checkAndClick, 3000);
}
});
const config = {
childList: true,
subtree: true
};
observer.observe(document.body, config);
}
})();