bug fix
This commit is contained in:
parent
2e2b166e08
commit
573cf9feb5
@ -171,6 +171,9 @@ const API = (() => {
|
||||
listRepairs: () => request('/list/repairs'),
|
||||
listDefects: () => request('/list/defects'),
|
||||
listShipments: () => request('/list/shipments'),
|
||||
listShipmentPlatforms: () => request('/shipments/platforms'),
|
||||
addShipmentPlatform: (name) => request('/shipments/platforms', { method: 'POST', body: JSON.stringify({ name }) }),
|
||||
deleteShipmentPlatform: (code) => request(`/shipments/platforms/${encodeURIComponent(code)}`, { method: 'DELETE' }),
|
||||
auditPdd: (params={}) => request('/audit/pdd' + buildQuery(params)),
|
||||
auditYt: (params={}) => request('/audit/yt' + buildQuery(params)),
|
||||
auditTx: (params={}) => request('/audit/tx' + buildQuery(params)),
|
||||
|
||||
@ -90,6 +90,14 @@
|
||||
<div class="page-actions" style="margin-top: 16px;">
|
||||
<button id="add-stock-btn" class="btn btn-primary" style="margin-right: 10px;">新增库存</button>
|
||||
<button id="import-stock-btn" class="btn btn-secondary" style="margin-right: 10px;">导入 Excel</button>
|
||||
<div style="display: inline-block; position: relative; margin-right: 10px;">
|
||||
<button id="export-btn" class="btn btn-secondary" onclick="InitialStock.toggleExportMenu(event)">导出 Excel ▼</button>
|
||||
<div id="export-menu" style="display: none; position: absolute; top: 100%; left: 0; background: white; border: 1px solid var(--border); border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 1000; min-width: 150px; margin-top: 4px;">
|
||||
<button onclick="InitialStock.exportAll()" style="display: block; width: 100%; padding: 8px 16px; border: none; background: none; text-align: left; cursor: pointer; font-size: 14px;" onmouseover="this.style.background='var(--hover)'" onmouseout="this.style.background='none'">导出全部</button>
|
||||
<button onclick="InitialStock.exportSelected()" style="display: block; width: 100%; padding: 8px 16px; border: none; background: none; text-align: left; cursor: pointer; font-size: 14px;" onmouseover="this.style.background='var(--hover)'" onmouseout="this.style.background='none'">导出选中</button>
|
||||
<button onclick="InitialStock.exportByFactory()" style="display: block; width: 100%; padding: 8px 16px; border: none; background: none; text-align: left; cursor: pointer; font-size: 14px;" onmouseover="this.style.background='var(--hover)'" onmouseout="this.style.background='none'">按工厂导出</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="download-template-btn" class="btn btn-secondary" style="margin-right: 10px;">下载模板</button>
|
||||
<button id="restore-backup-btn" class="btn btn-secondary" style="margin-right: 10px;">📦 数据恢复</button>
|
||||
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
|
||||
@ -325,6 +333,15 @@
|
||||
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') InitialStock.search();
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const exportMenu = document.getElementById('export-menu');
|
||||
const exportBtn = document.getElementById('export-btn');
|
||||
if (exportMenu && !exportMenu.contains(e.target) && e.target !== exportBtn) {
|
||||
exportMenu.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
loadList();
|
||||
loadFactories();
|
||||
}, 100);
|
||||
@ -656,6 +673,208 @@
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function toggleExportMenu(event) {
|
||||
event.stopPropagation();
|
||||
const menu = document.getElementById('export-menu');
|
||||
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
async function ensureXLSXLoaded() {
|
||||
if (typeof XLSX === 'undefined') {
|
||||
try {
|
||||
await loadXLSXLibrary();
|
||||
} catch (e) {
|
||||
throw new Error('无法加载 Excel 库,请刷新页面重试');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadXLSXLibrary() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof XLSX !== 'undefined') {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js';
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => reject(new Error('加载 XLSX 库失败'));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
async function exportAll() {
|
||||
try {
|
||||
document.getElementById('export-menu').style.display = 'none';
|
||||
|
||||
if (stockList.length === 0) {
|
||||
alert('没有数据可导出');
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureXLSXLoaded();
|
||||
|
||||
const exportData = stockList.map(item => ({
|
||||
'物料编码': item.material_code || '',
|
||||
'物料名称': item.material_name || '',
|
||||
'库存数量': item.stock_qty || 0,
|
||||
'单位': item.unit || 'pcs',
|
||||
'最小包装': item.min_package || 1,
|
||||
'供应商': item.supplier || '',
|
||||
'工厂': item.factory || '',
|
||||
'备注': item.remark || '',
|
||||
'更新时间': formatTime(item.updated_at)
|
||||
}));
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(exportData);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, '期初库存');
|
||||
|
||||
const colWidths = [
|
||||
{ wch: 15 },
|
||||
{ wch: 25 },
|
||||
{ wch: 10 },
|
||||
{ wch: 8 },
|
||||
{ wch: 10 },
|
||||
{ wch: 20 },
|
||||
{ wch: 15 },
|
||||
{ wch: 30 },
|
||||
{ wch: 18 }
|
||||
];
|
||||
ws['!cols'] = colWidths;
|
||||
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
||||
const filename = `期初库存_全部_${timestamp}.xlsx`;
|
||||
|
||||
XLSX.writeFile(wb, filename);
|
||||
|
||||
console.log(`成功导出 ${exportData.length} 条数据到 ${filename}`);
|
||||
|
||||
} catch (err) {
|
||||
console.error('导出失败:', err);
|
||||
alert('导出失败: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportSelected() {
|
||||
try {
|
||||
document.getElementById('export-menu').style.display = 'none';
|
||||
|
||||
const checked = document.querySelectorAll('.row-checkbox:checked');
|
||||
if (checked.length === 0) {
|
||||
alert('请先选择要导出的数据');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = Array.from(checked).map(cb => parseInt(cb.dataset.id));
|
||||
const selectedData = stockList.filter(item => selectedIds.includes(item.id));
|
||||
|
||||
await ensureXLSXLoaded();
|
||||
|
||||
const exportData = selectedData.map(item => ({
|
||||
'物料编码': item.material_code || '',
|
||||
'物料名称': item.material_name || '',
|
||||
'库存数量': item.stock_qty || 0,
|
||||
'单位': item.unit || 'pcs',
|
||||
'最小包装': item.min_package || 1,
|
||||
'供应商': item.supplier || '',
|
||||
'工厂': item.factory || '',
|
||||
'备注': item.remark || '',
|
||||
'更新时间': formatTime(item.updated_at)
|
||||
}));
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(exportData);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, '期初库存');
|
||||
|
||||
const colWidths = [
|
||||
{ wch: 15 },
|
||||
{ wch: 25 },
|
||||
{ wch: 10 },
|
||||
{ wch: 8 },
|
||||
{ wch: 10 },
|
||||
{ wch: 20 },
|
||||
{ wch: 15 },
|
||||
{ wch: 30 },
|
||||
{ wch: 18 }
|
||||
];
|
||||
ws['!cols'] = colWidths;
|
||||
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
||||
const filename = `期初库存_选中${exportData.length}条_${timestamp}.xlsx`;
|
||||
|
||||
XLSX.writeFile(wb, filename);
|
||||
|
||||
console.log(`成功导出 ${exportData.length} 条数据到 ${filename}`);
|
||||
|
||||
} catch (err) {
|
||||
console.error('导出失败:', err);
|
||||
alert('导出失败: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportByFactory() {
|
||||
try {
|
||||
document.getElementById('export-menu').style.display = 'none';
|
||||
|
||||
const factory = document.getElementById('factory-filter')?.value || '';
|
||||
|
||||
if (!factory) {
|
||||
alert('请先选择要导出的工厂');
|
||||
return;
|
||||
}
|
||||
|
||||
const factoryData = stockList.filter(item => item.factory === factory);
|
||||
|
||||
if (factoryData.length === 0) {
|
||||
alert(`工厂 "${factory}" 没有数据可导出`);
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureXLSXLoaded();
|
||||
|
||||
const exportData = factoryData.map(item => ({
|
||||
'物料编码': item.material_code || '',
|
||||
'物料名称': item.material_name || '',
|
||||
'库存数量': item.stock_qty || 0,
|
||||
'单位': item.unit || 'pcs',
|
||||
'最小包装': item.min_package || 1,
|
||||
'供应商': item.supplier || '',
|
||||
'工厂': item.factory || '',
|
||||
'备注': item.remark || '',
|
||||
'更新时间': formatTime(item.updated_at)
|
||||
}));
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(exportData);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, '期初库存');
|
||||
|
||||
const colWidths = [
|
||||
{ wch: 15 },
|
||||
{ wch: 25 },
|
||||
{ wch: 10 },
|
||||
{ wch: 8 },
|
||||
{ wch: 10 },
|
||||
{ wch: 20 },
|
||||
{ wch: 15 },
|
||||
{ wch: 30 },
|
||||
{ wch: 18 }
|
||||
];
|
||||
ws['!cols'] = colWidths;
|
||||
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
||||
const filename = `期初库存_${factory}_${timestamp}.xlsx`;
|
||||
|
||||
XLSX.writeFile(wb, filename);
|
||||
|
||||
console.log(`成功导出 ${exportData.length} 条数据到 ${filename}`);
|
||||
|
||||
} catch (err) {
|
||||
console.error('导出失败:', err);
|
||||
alert('导出失败: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function showRestoreDialog() {
|
||||
document.getElementById('restore-modal').style.display = 'flex';
|
||||
await loadBackupList();
|
||||
@ -777,6 +996,10 @@
|
||||
importExcel,
|
||||
closeRestoreModal,
|
||||
restoreBackup,
|
||||
deleteBackup
|
||||
deleteBackup,
|
||||
toggleExportMenu,
|
||||
exportAll,
|
||||
exportSelected,
|
||||
exportByFactory
|
||||
};
|
||||
})();
|
||||
|
||||
@ -91,6 +91,8 @@ Router.register('/shipments/query', async () => {
|
||||
<div class="result-item"><span class="label">机种:</span> <span class="badge">${data.platform_name || '未知'}</span></div>
|
||||
<div class="result-item"><span class="label">出货日期:</span> ${data.date}</div>
|
||||
<div class="result-item"><span class="label">箱号:</span> ${data.box}</div>
|
||||
${data.emmc ? `<div class="result-item"><span class="label">EMMC:</span> <span style="font-weight:600;color:var(--primary)">${data.emmc}</span></div>` : ''}
|
||||
${data.ddr ? `<div class="result-item"><span class="label">DDR:</span> <span style="font-weight:600;color:var(--primary)">${data.ddr}</span></div>` : ''}
|
||||
<div class="result-item"><span class="label">记录时间:</span> ${data.ts}</div>
|
||||
</div>
|
||||
`;
|
||||
@ -117,6 +119,8 @@ Router.register('/shipments/query', async () => {
|
||||
<div class="result-item"><span class="label">SN/MAC:</span> ${record.sn}</div>
|
||||
<div class="result-item"><span class="label">机种:</span> <span class="badge">${record.platform_name || '未知'}</span></div>
|
||||
<div class="result-item"><span class="label">出货日期:</span> ${record.date}</div>
|
||||
${record.emmc ? `<div class="result-item"><span class="label">EMMC:</span> <span style="font-weight:600;color:var(--primary)">${record.emmc}</span></div>` : ''}
|
||||
${record.ddr ? `<div class="result-item"><span class="label">DDR:</span> <span style="font-weight:600;color:var(--primary)">${record.ddr}</span></div>` : ''}
|
||||
<div class="result-item"><span class="label">记录时间:</span> ${record.ts}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
Router.register('/shipments/summary', async () => {
|
||||
setTimeout(async () => {
|
||||
const queryBtn = document.getElementById('summary-query-btn');
|
||||
const exportBtn = document.getElementById('summary-export-btn');
|
||||
const startDateInput = document.getElementById('start-date');
|
||||
const endDateInput = document.getElementById('end-date');
|
||||
const resultDiv = document.getElementById('summary-result');
|
||||
@ -89,7 +90,63 @@ Router.register('/shipments/summary', async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const performExport = async () => {
|
||||
const startDate = startDateInput?.value;
|
||||
const endDate = endDateInput?.value;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
alert('请选择开始和结束日期');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
exportBtn.disabled = true;
|
||||
exportBtn.textContent = '导出中...';
|
||||
|
||||
const url = `/api/shipments/export?start=${encodeURIComponent(startDate)}&end=${encodeURIComponent(endDate)}`;
|
||||
|
||||
// 使用 fetch 下载文件
|
||||
const res = await fetch(url, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.error || '导出失败');
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const contentDisposition = res.headers.get('Content-Disposition');
|
||||
let filename = `发货记录_${startDate}_至_${endDate}.xlsx`;
|
||||
if (contentDisposition) {
|
||||
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
|
||||
if (matches && matches[1]) {
|
||||
filename = matches[1].replace(/['"]/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const blob = await res.blob();
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
document.body.removeChild(a);
|
||||
|
||||
alert('导出成功!');
|
||||
} catch (e) {
|
||||
alert('导出失败:' + e.message);
|
||||
} finally {
|
||||
exportBtn.disabled = false;
|
||||
exportBtn.textContent = '导出Excel';
|
||||
}
|
||||
};
|
||||
|
||||
queryBtn?.addEventListener('click', performQuery);
|
||||
exportBtn?.addEventListener('click', performExport);
|
||||
|
||||
// 自动执行一次查询
|
||||
performQuery();
|
||||
@ -163,8 +220,9 @@ Router.register('/shipments/summary', async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<div class="actions" style="display:flex;gap:12px">
|
||||
<button class="btn btn-primary" id="summary-query-btn">查询</button>
|
||||
<button class="btn" id="summary-export-btn" style="background:#10b981;color:white">导出Excel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,6 +1,108 @@
|
||||
Router.register('/upload/shipments', async () => {
|
||||
setTimeout(() => {
|
||||
const manualStatus = document.getElementById('ship-manual-status');
|
||||
const defaultPlatformNameMap = {pdd: '拼多多', yt: '圆通', tx: '兔喜', mt: '美团', drf: '大润发', std: '标准版'};
|
||||
|
||||
const renderPlatformOptions = (selectEl, list, selectedValue = '') => {
|
||||
if (!selectEl) return;
|
||||
const optsHtml = [
|
||||
'<option value="">请选择机种</option>',
|
||||
...(list || []).map(o => `<option value="${o.value}">${o.label}</option>`)
|
||||
].join('');
|
||||
selectEl.innerHTML = optsHtml;
|
||||
if (selectedValue) selectEl.value = selectedValue;
|
||||
};
|
||||
|
||||
const loadPlatforms = async (selectedManual = '', selectedImport = '') => {
|
||||
try {
|
||||
const res = await API.listShipmentPlatforms();
|
||||
const list = (res && res.list) ? res.list : [];
|
||||
renderPlatformOptions(document.getElementById('ship-manual-platform'), list, selectedManual);
|
||||
renderPlatformOptions(document.getElementById('ship-platform'), list, selectedImport);
|
||||
} catch (e) {
|
||||
// 失败时不阻塞页面,保持当前 options(可能是旧的静态内容)
|
||||
console.warn('加载机种列表失败:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const showManageModal = async () => {
|
||||
let list = [];
|
||||
try {
|
||||
const res = await API.listShipmentPlatforms();
|
||||
list = (res && res.list) ? res.list : [];
|
||||
} catch (e) {
|
||||
API.toast('加载机种失败');
|
||||
return;
|
||||
}
|
||||
|
||||
const custom = list.filter(x => !x.is_default);
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'ship-platform-manage-modal';
|
||||
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px';
|
||||
modal.innerHTML = `
|
||||
<div style="background:var(--surface);border-radius:12px;width:90%;max-width:520px;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 10px 30px rgba(0,0,0,0.2)">
|
||||
<div style="padding:14px 18px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
|
||||
<div style="font-weight:700">机种管理(仅自定义可删除)</div>
|
||||
<button id="ship-platform-manage-close" class="btn btn-secondary" style="padding:4px 10px">关闭</button>
|
||||
</div>
|
||||
<div style="padding:16px;overflow:auto;flex:1">
|
||||
${custom.length ? `
|
||||
<div style="font-size:12px;color:var(--text-2);margin-bottom:10px">共 ${custom.length} 个自定义机种</div>
|
||||
<ul class="list" style="margin:0">
|
||||
${custom.map(x => `
|
||||
<li style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="min-width:0">
|
||||
<div style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${x.label}</div>
|
||||
<div style="font-size:11px;color:var(--text-2);font-family:monospace">${x.value}</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary ship-platform-delete" data-code="${x.value}" style="background:var(--danger);color:white">删除</button>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
` : `
|
||||
<div style="text-align:center;padding:30px 0;color:var(--text-2)">暂无自定义机种</div>
|
||||
`}
|
||||
</div>
|
||||
<div style="padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:10px">
|
||||
<button class="btn" id="ship-platform-manage-add">新增机种</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const close = () => modal.remove();
|
||||
modal.addEventListener('click', (e) => { if (e.target === modal) close(); });
|
||||
modal.querySelector('#ship-platform-manage-close')?.addEventListener('click', close);
|
||||
|
||||
modal.querySelector('#ship-platform-manage-add')?.addEventListener('click', async () => {
|
||||
const name = (prompt('请输入自定义机种名称') || '').trim();
|
||||
if (!name) return;
|
||||
try {
|
||||
const added = await API.addShipmentPlatform(name);
|
||||
if (added && added.ok) {
|
||||
API.toast('已添加机种:' + (added.label || name));
|
||||
close();
|
||||
await loadPlatforms(added.value, added.value);
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
modal.querySelectorAll('.ship-platform-delete').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const code = btn.dataset.code;
|
||||
const label = btn.closest('li')?.querySelector('div div')?.textContent || code;
|
||||
if (!confirm(`确定删除自定义机种:${label}?\n\n删除后下拉将不再显示该机种。`)) return;
|
||||
try {
|
||||
const r = await API.deleteShipmentPlatform(code);
|
||||
if (r && r.ok) {
|
||||
API.toast('删除成功');
|
||||
close();
|
||||
await loadPlatforms();
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 设置默认日期为今天
|
||||
const dateInput = document.getElementById('ship-date');
|
||||
@ -8,6 +110,11 @@ Router.register('/upload/shipments', async () => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
dateInput.value = today;
|
||||
}
|
||||
|
||||
// 动态加载机种下拉,并支持自定义新增
|
||||
loadPlatforms();
|
||||
|
||||
document.getElementById('ship-manage-platforms')?.addEventListener('click', showManageModal);
|
||||
|
||||
// 手动录入提交
|
||||
const btn = document.getElementById('ship-upload');
|
||||
@ -54,7 +161,7 @@ Router.register('/upload/shipments', async () => {
|
||||
|
||||
await API.uploadShipments(payload);
|
||||
|
||||
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜', mt: '美团', drf: '大润发', std: '标准版'}[platform] || platform;
|
||||
const platformName = defaultPlatformNameMap[platform] || platform;
|
||||
manualStatus.textContent = `✓ 录入成功!机种:${platformName},数量:${qty}`;
|
||||
manualStatus.className = 'success';
|
||||
|
||||
@ -155,7 +262,7 @@ Router.register('/upload/shipments', async () => {
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜', mt: '美团', drf: '大润发', std: '标准版'}[platform] || platform;
|
||||
const platformName = defaultPlatformNameMap[platform] || platform;
|
||||
fileStatus.textContent = `✓ 上传成功!机种:${platformName},共导入${data.count}个箱次,${data.total_qty}个SN`;
|
||||
fileStatus.className = 'success';
|
||||
fileInput.value = '';
|
||||
@ -234,6 +341,9 @@ Router.register('/upload/shipments', async () => {
|
||||
<div id="shipments-page">
|
||||
<div class="page-header">
|
||||
<h1>发货记录</h1>
|
||||
<div class="page-actions">
|
||||
<button class="btn btn-secondary" id="ship-manage-platforms" type="button">机种管理</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
|
||||
368
server/app.py
368
server/app.py
@ -19,6 +19,16 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DB_PATH = os.path.join(BASE_DIR, 'data.db')
|
||||
FRONTEND_DIR = os.path.join(os.path.dirname(BASE_DIR), 'frontend')
|
||||
|
||||
# 发货机种(默认选项 + 自定义表)
|
||||
DEFAULT_SHIPMENT_PLATFORMS = {
|
||||
'pdd': '拼多多',
|
||||
'yt': '圆通',
|
||||
'tx': '兔喜',
|
||||
'mt': '美团',
|
||||
'drf': '大润发',
|
||||
'std': '标准版',
|
||||
}
|
||||
|
||||
app = Flask(__name__, static_folder=FRONTEND_DIR, static_url_path='')
|
||||
app.config['SECRET_KEY'] = os.environ.get('APP_SECRET', 'change-me')
|
||||
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 限制上传文件大小为50MB
|
||||
@ -110,6 +120,25 @@ def init_db():
|
||||
date TEXT,
|
||||
qty INTEGER,
|
||||
receiver TEXT,
|
||||
ts TEXT,
|
||||
emmc TEXT,
|
||||
ddr TEXT
|
||||
)''')
|
||||
# 为已存在的表添加 emmc 和 ddr 列(如果不存在)
|
||||
try:
|
||||
c.execute('ALTER TABLE shipments ADD COLUMN emmc TEXT')
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
c.execute('ALTER TABLE shipments ADD COLUMN ddr TEXT')
|
||||
except:
|
||||
pass
|
||||
|
||||
# 发货机种类型(自定义项)
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS shipment_platforms(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT UNIQUE NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
ts TEXT
|
||||
)''')
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS devices(
|
||||
@ -917,6 +946,49 @@ def upload_avatar():
|
||||
c.execute('UPDATE users SET avatar=? WHERE id=?', (avatar_url, uid))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def _make_platform_code():
|
||||
"""生成一个短且稳定可用的机种 code(避免与默认 code 冲突)"""
|
||||
# 12位随机 hex 足够用且不会太长
|
||||
import secrets
|
||||
return 'cust_' + secrets.token_hex(6)
|
||||
|
||||
|
||||
def list_shipment_platforms():
|
||||
"""返回发货机种选项(包含默认 + 自定义)"""
|
||||
opts = [{'code': k, 'name': v, 'is_default': True} for k, v in DEFAULT_SHIPMENT_PLATFORMS.items()]
|
||||
try:
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT code, name FROM shipment_platforms ORDER BY id DESC')
|
||||
rows = [dict(r) for r in c.fetchall()]
|
||||
conn.close()
|
||||
# 自定义排在后面,防止覆盖默认同 code(理论上不会发生)
|
||||
for r in rows:
|
||||
opts.append({'code': r['code'], 'name': r['name'], 'is_default': False})
|
||||
except Exception:
|
||||
pass
|
||||
return opts
|
||||
|
||||
|
||||
def shipment_platform_display_name(code):
|
||||
"""将 platform code 映射为展示名称(默认映射 + 自定义表)"""
|
||||
if not code:
|
||||
return ''
|
||||
if code in DEFAULT_SHIPMENT_PLATFORMS:
|
||||
return DEFAULT_SHIPMENT_PLATFORMS[code]
|
||||
try:
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT name FROM shipment_platforms WHERE code=?', (code,))
|
||||
row = c.fetchone()
|
||||
conn.close()
|
||||
if row and row['name']:
|
||||
return row['name']
|
||||
except Exception:
|
||||
pass
|
||||
return code
|
||||
|
||||
log('upload_avatar', f'上传头像: {new_filename}')
|
||||
return jsonify({'ok': True, 'avatar_url': avatar_url})
|
||||
@ -2688,7 +2760,7 @@ def upload_shipments():
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
|
||||
platform_name = shipment_platform_display_name(platform)
|
||||
log_data = {'date': date, 'qty': qty, 'to': to, 'platform': platform}
|
||||
if box_no:
|
||||
log_data['box_no'] = box_no
|
||||
@ -2698,6 +2770,91 @@ def upload_shipments():
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.get('/api/shipments/platforms')
|
||||
@require_login
|
||||
def get_shipment_platforms():
|
||||
"""获取发货机种选项(默认 + 自定义)"""
|
||||
opts = list_shipment_platforms()
|
||||
# 前端需要统一格式
|
||||
return jsonify({'list': [{'value': o['code'], 'label': o['name'], 'is_default': o['is_default']} for o in opts]})
|
||||
|
||||
|
||||
@app.post('/api/shipments/platforms')
|
||||
@require_login
|
||||
@require_any_role('admin','superadmin')
|
||||
def add_shipment_platform():
|
||||
"""添加自定义发货机种"""
|
||||
data = request.get_json() or {}
|
||||
name = (data.get('name') or '').strip()
|
||||
if not name:
|
||||
return jsonify({'error': 'invalid payload'}), 400
|
||||
if len(name) > 50:
|
||||
return jsonify({'error': '机种名称过长'}), 400
|
||||
|
||||
# 若与默认名称完全一致,直接返回默认项
|
||||
for code, label in DEFAULT_SHIPMENT_PLATFORMS.items():
|
||||
if label == name:
|
||||
return jsonify({'ok': True, 'value': code, 'label': label, 'is_default': True})
|
||||
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
try:
|
||||
# 若已存在同名自定义项,直接返回已有 code
|
||||
c.execute('SELECT code, name FROM shipment_platforms WHERE name=?', (name,))
|
||||
row = c.fetchone()
|
||||
if row:
|
||||
conn.close()
|
||||
return jsonify({'ok': True, 'value': row['code'], 'label': row['name'], 'is_default': False})
|
||||
|
||||
code = _make_platform_code()
|
||||
c.execute('INSERT INTO shipment_platforms(code, name, ts) VALUES(?,?,?)', (code, name, get_beijing_time()))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
log('add_shipment_platform', json.dumps({'code': code, 'name': name}))
|
||||
notify_superadmin('新增发货机种', f"机种: {name}({code})")
|
||||
return jsonify({'ok': True, 'value': code, 'label': name, 'is_default': False})
|
||||
except Exception as e:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.delete('/api/shipments/platforms/<code>')
|
||||
@require_login
|
||||
@require_any_role('admin','superadmin')
|
||||
def delete_shipment_platform(code):
|
||||
"""删除自定义发货机种(默认机种不可删除)"""
|
||||
code = (code or '').strip()
|
||||
if not code:
|
||||
return jsonify({'error': 'invalid payload'}), 400
|
||||
if code in DEFAULT_SHIPMENT_PLATFORMS:
|
||||
return jsonify({'error': '默认机种不可删除'}), 400
|
||||
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute('SELECT id, name FROM shipment_platforms WHERE code=?', (code,))
|
||||
row = c.fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return jsonify({'ok': True, 'deleted': 0})
|
||||
|
||||
c.execute('DELETE FROM shipment_platforms WHERE code=?', (code,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
log('delete_shipment_platform', json.dumps({'code': code, 'name': row['name']}))
|
||||
notify_superadmin('删除发货机种', f"机种: {row['name']}({code})")
|
||||
return jsonify({'ok': True, 'deleted': 1})
|
||||
except Exception as e:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# collect
|
||||
@app.get('/api/collect/devices')
|
||||
@require_login
|
||||
@ -3764,7 +3921,7 @@ def query_shipment_by_sn():
|
||||
# 解析 JSON 数据
|
||||
shipment_info = json.loads(result)
|
||||
platform = shipment_info.get('platform', 'pdd') # 默认拼多多
|
||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
|
||||
platform_name = shipment_platform_display_name(platform)
|
||||
return jsonify({
|
||||
'found': True,
|
||||
'sn': sn,
|
||||
@ -3772,7 +3929,9 @@ def query_shipment_by_sn():
|
||||
'box': shipment_info.get('box'),
|
||||
'platform': platform,
|
||||
'platform_name': platform_name,
|
||||
'ts': shipment_info.get('ts')
|
||||
'ts': shipment_info.get('ts'),
|
||||
'emmc': shipment_info.get('emmc', ''),
|
||||
'ddr': shipment_info.get('ddr', '')
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
@ -3809,14 +3968,16 @@ def query_shipment_by_box():
|
||||
shipment_info = json.loads(data)
|
||||
if shipment_info.get('box') == box_no:
|
||||
platform = shipment_info.get('platform', 'pdd')
|
||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
|
||||
platform_name = shipment_platform_display_name(platform)
|
||||
matched_records.append({
|
||||
'sn': sn.decode('utf-8') if isinstance(sn, bytes) else sn,
|
||||
'date': shipment_info.get('date'),
|
||||
'box': shipment_info.get('box'),
|
||||
'platform': platform,
|
||||
'platform_name': platform_name,
|
||||
'ts': shipment_info.get('ts')
|
||||
'ts': shipment_info.get('ts'),
|
||||
'emmc': shipment_info.get('emmc', ''),
|
||||
'ddr': shipment_info.get('ddr', '')
|
||||
})
|
||||
except:
|
||||
continue
|
||||
@ -4022,6 +4183,130 @@ def clear_shipments_redis():
|
||||
return jsonify({'error': f'清空失败:{str(e)}'}), 500
|
||||
|
||||
|
||||
@app.get('/api/shipments/export')
|
||||
@require_login
|
||||
def export_shipments():
|
||||
"""导出发货记录为Excel文件"""
|
||||
try:
|
||||
start_date = request.args.get('start')
|
||||
end_date = request.args.get('end')
|
||||
|
||||
if not start_date or not end_date:
|
||||
return jsonify({'error': '请提供开始和结束日期'}), 400
|
||||
|
||||
import openpyxl
|
||||
from openpyxl.styles import Font, Alignment, PatternFill
|
||||
import json
|
||||
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
# 查询指定日期范围内的发货记录(按箱号分组)
|
||||
c.execute('''
|
||||
SELECT id, date, receiver, emmc, ddr, ts
|
||||
FROM shipments
|
||||
WHERE date >= ? AND date <= ?
|
||||
ORDER BY date, id
|
||||
''', (start_date, end_date))
|
||||
|
||||
records = [dict(r) for r in c.fetchall()]
|
||||
conn.close()
|
||||
|
||||
if not records:
|
||||
return jsonify({'error': '该时间段内没有发货记录'}), 404
|
||||
|
||||
# 从 Redis 获取 SN 数据
|
||||
try:
|
||||
r = get_redis()
|
||||
redis_key = 'shipment_sn_mapping'
|
||||
all_sn_data = r.hgetall(redis_key)
|
||||
except:
|
||||
all_sn_data = {}
|
||||
|
||||
# 创建 Excel 工作簿
|
||||
wb = openpyxl.Workbook()
|
||||
ws = wb.active
|
||||
ws.title = '发货记录'
|
||||
|
||||
# 设置表头样式
|
||||
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
|
||||
header_font = Font(bold=True, color='FFFFFF')
|
||||
header_alignment = Alignment(horizontal='center', vertical='center')
|
||||
|
||||
# 创建表头
|
||||
headers = ['出货日期', '箱号'] + [f'SN{i}' for i in range(1, 21)] + ['EMMC', 'DDR']
|
||||
for col_idx, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col_idx, value=header)
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = header_alignment
|
||||
|
||||
# 填充数据
|
||||
row_idx = 2
|
||||
for record in records:
|
||||
# 提取箱号
|
||||
receiver = record['receiver'] or ''
|
||||
box_no = receiver.replace('箱号:', '').strip()
|
||||
|
||||
# 从 Redis 获取该箱号的所有 SN
|
||||
sns = []
|
||||
for sn, data in all_sn_data.items():
|
||||
try:
|
||||
sn_str = sn.decode('utf-8') if isinstance(sn, bytes) else sn
|
||||
data_str = data.decode('utf-8') if isinstance(data, bytes) else data
|
||||
info = json.loads(data_str)
|
||||
if info.get('box') == box_no:
|
||||
sns.append(sn_str)
|
||||
except:
|
||||
continue
|
||||
|
||||
# 写入数据行
|
||||
ws.cell(row=row_idx, column=1, value=record['date'])
|
||||
ws.cell(row=row_idx, column=2, value=box_no)
|
||||
|
||||
# 写入 SN(最多20个)
|
||||
for i, sn in enumerate(sns[:20], 3):
|
||||
ws.cell(row=row_idx, column=i, value=sn)
|
||||
|
||||
# 写入 EMMC 和 DDR
|
||||
ws.cell(row=row_idx, column=23, value=record.get('emmc', ''))
|
||||
ws.cell(row=row_idx, column=24, value=record.get('ddr', ''))
|
||||
|
||||
row_idx += 1
|
||||
|
||||
# 调整列宽
|
||||
ws.column_dimensions['A'].width = 12 # 出货日期
|
||||
ws.column_dimensions['B'].width = 20 # 箱号
|
||||
for i in range(3, 23): # SN1-SN20
|
||||
ws.column_dimensions[openpyxl.utils.get_column_letter(i)].width = 25
|
||||
ws.column_dimensions['W'].width = 10 # EMMC
|
||||
ws.column_dimensions['X'].width = 10 # DDR
|
||||
|
||||
# 保存到内存
|
||||
from io import BytesIO
|
||||
output = BytesIO()
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
|
||||
# 生成文件名
|
||||
filename = f'发货记录_{start_date}_至_{end_date}.xlsx'
|
||||
|
||||
log('export_shipments', f'start={start_date}, end={end_date}, records={len(records)}')
|
||||
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log('export_shipments_error', str(e))
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({'error': f'导出失败:{str(e)}'}), 500
|
||||
|
||||
|
||||
@app.route('/api/upload/shipments-file', methods=['POST'])
|
||||
@require_login
|
||||
@require_any_role('admin','superadmin')
|
||||
@ -4033,8 +4318,9 @@ def upload_shipments_file():
|
||||
if not f:
|
||||
return jsonify({'error': '请选择文件'}), 400
|
||||
|
||||
if not platform or platform not in ['pdd', 'yt', 'tx']:
|
||||
return jsonify({'error': '请选择机种(拼多多/圆通/兔喜)'}), 400
|
||||
allowed = set([o['code'] for o in list_shipment_platforms()])
|
||||
if not platform or platform not in allowed:
|
||||
return jsonify({'error': '请选择有效的机种类型(可在下拉中新增自定义机种)'}), 400
|
||||
|
||||
name = secure_filename(f.filename or '')
|
||||
ext = (name.split('.')[-1] or '').lower()
|
||||
@ -4056,6 +4342,8 @@ def upload_shipments_file():
|
||||
# 找到列索引
|
||||
date_idx = next((i for i, h in enumerate(header) if '出货日期' in h or '发货日期' in h or 'date' in header_lower[i]), None)
|
||||
box_idx = next((i for i, h in enumerate(header) if '箱号' in h or 'box' in header_lower[i]), None)
|
||||
emmc_idx = next((i for i, h in enumerate(header) if 'emmc' in h.lower()), None)
|
||||
ddr_idx = next((i for i, h in enumerate(header) if 'ddr' in h.lower()), None)
|
||||
|
||||
if date_idx is None or box_idx is None:
|
||||
return jsonify({'error': '缺少必需的列'}), 400
|
||||
@ -4064,8 +4352,10 @@ def upload_shipments_file():
|
||||
sn_cols = [(i, h) for i, h in enumerate(header) if h.startswith('SN') and h[2:].isdigit()]
|
||||
sn_indices = sorted(sn_cols, key=lambda x: int(x[1][2:])) # 按 SN 后面的数字排序
|
||||
|
||||
# 记录上一个有效的日期(用于处理合并单元格)
|
||||
# 记录上一个有效的日期、EMMC、DDR(用于处理合并单元格)
|
||||
last_valid_date = None
|
||||
last_valid_emmc = None
|
||||
last_valid_ddr = None
|
||||
|
||||
for line in lines[1:]:
|
||||
parts = [p.strip() for p in line.split(',')]
|
||||
@ -4085,6 +4375,22 @@ def upload_shipments_file():
|
||||
# 处理箱号
|
||||
box = parts[box_idx] if box_idx < len(parts) and parts[box_idx] else ''
|
||||
|
||||
# 处理 EMMC(支持合并单元格)
|
||||
current_emmc = parts[emmc_idx] if emmc_idx is not None and emmc_idx < len(parts) and parts[emmc_idx] else ''
|
||||
if current_emmc:
|
||||
last_valid_emmc = current_emmc
|
||||
emmc = current_emmc
|
||||
else:
|
||||
emmc = last_valid_emmc or ''
|
||||
|
||||
# 处理 DDR(支持合并单元格)
|
||||
current_ddr = parts[ddr_idx] if ddr_idx is not None and ddr_idx < len(parts) and parts[ddr_idx] else ''
|
||||
if current_ddr:
|
||||
last_valid_ddr = current_ddr
|
||||
ddr = current_ddr
|
||||
else:
|
||||
ddr = last_valid_ddr or ''
|
||||
|
||||
# 如果没有日期或箱号,跳过这行
|
||||
if not date or not box:
|
||||
continue
|
||||
@ -4101,7 +4407,9 @@ def upload_shipments_file():
|
||||
'date': date,
|
||||
'box': box,
|
||||
'sns': sns,
|
||||
'qty': len(sns)
|
||||
'qty': len(sns),
|
||||
'emmc': emmc,
|
||||
'ddr': ddr
|
||||
})
|
||||
else:
|
||||
import openpyxl
|
||||
@ -4120,6 +4428,8 @@ def upload_shipments_file():
|
||||
# 找到列索引
|
||||
date_idx = next((i for i, h in enumerate(header) if '出货日期' in h or '发货日期' in h or 'date' in header_lower[i]), None)
|
||||
box_idx = next((i for i, h in enumerate(header) if '箱号' in h or 'box' in header_lower[i]), None)
|
||||
emmc_idx = next((i for i, h in enumerate(header) if 'emmc' in h.lower()), None)
|
||||
ddr_idx = next((i for i, h in enumerate(header) if 'ddr' in h.lower()), None)
|
||||
|
||||
if date_idx is None or box_idx is None:
|
||||
wb.close()
|
||||
@ -4129,8 +4439,10 @@ def upload_shipments_file():
|
||||
sn_cols = [(i, h) for i, h in enumerate(header) if h.startswith('SN') and h[2:].isdigit()]
|
||||
sn_indices = sorted(sn_cols, key=lambda x: int(x[1][2:])) # 按 SN 后面的数字排序
|
||||
|
||||
# 记录上一个有效的日期(用于处理合并单元格)
|
||||
# 记录上一个有效的日期、EMMC、DDR(用于处理合并单元格)
|
||||
last_valid_date = None
|
||||
last_valid_emmc = None
|
||||
last_valid_ddr = None
|
||||
|
||||
# 读取数据行
|
||||
for row in ws.iter_rows(min_row=2, values_only=True):
|
||||
@ -4168,6 +4480,24 @@ def upload_shipments_file():
|
||||
# 处理箱号
|
||||
box = str(row[box_idx]).strip() if box_idx < len(row) and row[box_idx] else ''
|
||||
|
||||
# 处理 EMMC(支持合并单元格)
|
||||
emmc_value = row[emmc_idx] if emmc_idx is not None and emmc_idx < len(row) else None
|
||||
current_emmc = str(emmc_value).strip() if emmc_value and str(emmc_value) != 'None' else ''
|
||||
if current_emmc:
|
||||
last_valid_emmc = current_emmc
|
||||
emmc = current_emmc
|
||||
else:
|
||||
emmc = last_valid_emmc or ''
|
||||
|
||||
# 处理 DDR(支持合并单元格)
|
||||
ddr_value = row[ddr_idx] if ddr_idx is not None and ddr_idx < len(row) else None
|
||||
current_ddr = str(ddr_value).strip() if ddr_value and str(ddr_value) != 'None' else ''
|
||||
if current_ddr:
|
||||
last_valid_ddr = current_ddr
|
||||
ddr = current_ddr
|
||||
else:
|
||||
ddr = last_valid_ddr or ''
|
||||
|
||||
# 如果没有日期或箱号,跳过这行
|
||||
if not date or not box or box == 'None':
|
||||
continue
|
||||
@ -4186,7 +4516,9 @@ def upload_shipments_file():
|
||||
'date': date,
|
||||
'box': box,
|
||||
'sns': sns,
|
||||
'qty': len(sns)
|
||||
'qty': len(sns),
|
||||
'emmc': emmc,
|
||||
'ddr': ddr
|
||||
})
|
||||
|
||||
wb.close()
|
||||
@ -4202,8 +4534,10 @@ def upload_shipments_file():
|
||||
total_qty = 0
|
||||
for r in rows:
|
||||
receiver_info = f"箱号:{r['box']}"
|
||||
c.execute('INSERT INTO shipments(date, qty, receiver, ts) VALUES(?,?,?,?)',
|
||||
(r['date'], r['qty'], receiver_info, now))
|
||||
emmc_value = r.get('emmc', '')
|
||||
ddr_value = r.get('ddr', '')
|
||||
c.execute('INSERT INTO shipments(date, qty, receiver, ts, emmc, ddr) VALUES(?,?,?,?,?,?)',
|
||||
(r['date'], r['qty'], receiver_info, now, emmc_value, ddr_value))
|
||||
total_qty += r['qty']
|
||||
|
||||
conn.commit()
|
||||
@ -4224,14 +4558,18 @@ def upload_shipments_file():
|
||||
sns = row_data['sns']
|
||||
|
||||
# 为每个 SN/MAC 创建映射记录
|
||||
emmc = row_data.get('emmc', '')
|
||||
ddr = row_data.get('ddr', '')
|
||||
for sn in sns:
|
||||
if sn: # 确保 SN 不为空
|
||||
# 存储格式: MAC -> JSON(date, box, platform, timestamp)
|
||||
# 存储格式: MAC -> JSON(date, box, platform, timestamp, emmc, ddr)
|
||||
shipment_info = json.dumps({
|
||||
'date': date,
|
||||
'box': box,
|
||||
'platform': platform,
|
||||
'ts': now
|
||||
'ts': now,
|
||||
'emmc': emmc,
|
||||
'ddr': ddr
|
||||
}, ensure_ascii=False)
|
||||
pipe.hset(redis_key, sn, shipment_info)
|
||||
redis_count += 1
|
||||
|
||||
BIN
server/data.db
BIN
server/data.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user