@@ -1098,6 +1100,86 @@ const Upload = (() => {
}
};
});
+
+ // 快递单号点击查询物流
+ document.querySelectorAll('.tracking-link').forEach(link => {
+ link.onclick = async () => {
+ const trackingNumber = link.dataset.tracking;
+ if(!trackingNumber) return;
+
+ // 显示加载弹窗
+ const modal = document.createElement('div');
+ modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000';
+ modal.innerHTML = `
+
+ `;
+ document.body.appendChild(modal);
+ document.getElementById('tracking-modal-close').onclick = () => modal.remove();
+ modal.onclick = (e) => { if(e.target === modal) modal.remove(); };
+
+ try {
+ const result = await API.queryTracking(trackingNumber);
+ const contentEl = document.getElementById('tracking-content');
+
+ if(result.ok && result.data && result.data.length > 0) {
+ // 快递公司名称映射
+ const comNames = {
+ 'shunfeng': '顺丰速运', 'yuantong': '圆通速递', 'zhongtong': '中通快递',
+ 'yunda': '韵达快递', 'jd': '京东物流', 'ems': 'EMS', 'shentong': '申通快递',
+ 'debangkuaidi': '德邦快递', 'auto': '快递'
+ };
+ const comName = comNames[result.com] || result.com || '快递';
+ const stateNames = {'0': '在途', '1': '揽收', '2': '疑难', '3': '签收', '4': '退签', '5': '派件', '6': '退回', '7': '转投'};
+ const stateName = stateNames[result.state] || '未知';
+ const stateColor = result.state === '3' ? 'var(--success)' : (result.state === '5' ? 'var(--primary)' : 'var(--text-2)');
+
+ contentEl.innerHTML = `
+
+ `;
+ } else {
+ contentEl.innerHTML = `
+
+ `;
+ }
+ } catch(e) {
+ document.getElementById('tracking-content').innerHTML = `
+
+ `;
+ }
+ };
+ });
};
// 更新全选框状态
@@ -1163,7 +1245,8 @@ const Upload = (() => {
let modal = document.getElementById(modalId);
if(modal) modal.remove();
- const imagesHtml = (record.images || []).length > 0
+ const images = record.images || [];
+ const imagesHtml = images.length > 0
? `
上传时间
${ts}
@@ -1258,29 +1366,87 @@ const Upload = (() => {
modal.querySelector('div').onclick = (e) => { if(e.target === modal.querySelector('div')) modal.remove(); };
if(isEdit) {
- const editDefectSelect = document.getElementById('edit-repair-defect');
- const editDefectOther = document.getElementById('edit-repair-defect-other');
+ const editDefectInput = document.getElementById('edit-repair-defect');
const editMethodRequired = document.getElementById('edit-method-required');
- // 不良原因选择变化
- editDefectSelect.onchange = () => {
- if(editDefectSelect.value === '其他') {
- editDefectOther.style.display = 'block';
- } else {
- editDefectOther.style.display = 'none';
+ // 图片管理相关
+ let keepImages = [...images]; // 要保留的现有图片
+ let newImageFiles = []; // 新添加的图片文件
+
+ // 删除现有图片
+ document.querySelectorAll('.edit-image-delete').forEach(btn => {
+ btn.onclick = (e) => {
+ e.stopPropagation();
+ const filename = btn.dataset.filename;
+ keepImages = keepImages.filter(img => img !== filename);
+ btn.closest('.edit-image-item').remove();
+ };
+ });
+
+ // 添加新图片
+ const editDropzone = document.getElementById('edit-image-dropzone');
+ const editNewImagesInput = document.getElementById('edit-new-images');
+ const editNewImagesPreview = document.getElementById('edit-new-images-preview');
+
+ editDropzone.onclick = () => editNewImagesInput.click();
+ editDropzone.ondragover = (e) => { e.preventDefault(); editDropzone.style.borderColor = 'var(--primary)'; };
+ editDropzone.ondragleave = () => { editDropzone.style.borderColor = 'rgba(79,140,255,0.4)'; };
+ editDropzone.ondrop = (e) => {
+ e.preventDefault();
+ editDropzone.style.borderColor = 'rgba(79,140,255,0.4)';
+ handleNewImages(e.dataTransfer.files);
+ };
+
+ editNewImagesInput.onchange = () => handleNewImages(editNewImagesInput.files);
+
+ function handleNewImages(files) {
+ for(const file of files) {
+ if(!file.type.startsWith('image/')) continue;
+ if(keepImages.length + newImageFiles.length >= 10) {
+ API.toast('最多10张图片');
+ break;
+ }
+ newImageFiles.push(file);
+
+ // 预览
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const idx = newImageFiles.length - 1;
+ const div = document.createElement('div');
+ div.className = 'new-image-item';
+ div.dataset.idx = idx;
+ div.style.cssText = 'position:relative;width:80px;height:80px;border-radius:6px;overflow:hidden;border:1px solid var(--border)';
+ div.innerHTML = `
+

+
+ `;
+ editNewImagesPreview.appendChild(div);
+
+ div.querySelector('.new-image-delete').onclick = (ev) => {
+ ev.stopPropagation();
+ const i = parseInt(div.dataset.idx);
+ newImageFiles[i] = null;
+ div.remove();
+ };
+ };
+ reader.readAsDataURL(file);
}
- editMethodRequired.style.display = editDefectSelect.value === '无不良' ? 'none' : 'inline';
+ }
+
+ // 不良原因输入变化
+ editDefectInput.oninput = () => {
+ const defectValue = editDefectInput.value.trim();
+ editMethodRequired.style.display = defectValue === '无不良' ? 'none' : 'inline';
};
document.getElementById('edit-repair-cancel').onclick = () => modal.remove();
document.getElementById('edit-repair-save').onclick = async () => {
const newSn = document.getElementById('edit-repair-sn').value.trim();
const newMachineType = document.getElementById('edit-repair-machine-type').value;
- let newDefect = editDefectSelect.value;
- if(newDefect === '其他') {
- newDefect = editDefectOther.value.trim();
- }
+ const newSymptom = document.getElementById('edit-repair-symptom').value.trim();
+ const newDefect = editDefectInput.value.trim();
const newMethod = document.getElementById('edit-repair-method').value.trim();
+ const newTracking = document.getElementById('edit-repair-tracking').value.trim();
// 验证
if(!newSn || newSn.length < 6) {
@@ -1291,29 +1457,54 @@ const Upload = (() => {
API.toast('请选择机种');
return;
}
+ if(!newSymptom) {
+ API.toast('请输入不良现象');
+ return;
+ }
if(!newDefect) {
- API.toast('请选择或输入不良原因');
+ API.toast('请输入不良原因');
return;
}
if(newDefect !== '无不良' && !newMethod) {
- API.toast('选择了不良原因时,维修方法为必填项');
+ API.toast('填写了不良原因时,维修方法为必填项');
return;
}
try {
+ // 先更新基本信息
const result = await API.updateRepairRecord(id, {
sn: newSn,
machine_type: newMachineType,
+ defect_symptom: newSymptom,
defect_reason: newDefect,
- repair_method: newMethod
+ repair_method: newMethod,
+ tracking_number: newTracking
});
- if(result.ok) {
- API.toast('保存成功');
- modal.remove();
- refreshRepairsList();
- } else {
+
+ if(!result.ok) {
API.toast(result.error || '保存失败');
+ return;
}
+
+ // 如果图片有变化,更新图片
+ const actualNewFiles = newImageFiles.filter(f => f !== null);
+ const imagesChanged = keepImages.length !== images.length || actualNewFiles.length > 0;
+
+ if(imagesChanged) {
+ const formData = new FormData();
+ keepImages.forEach(img => formData.append('keep_images', img));
+ actualNewFiles.forEach(file => formData.append('new_images', file));
+
+ const imgResult = await API.updateRepairImages(id, formData);
+ if(!imgResult.ok) {
+ API.toast('图片更新失败: ' + (imgResult.error || '未知错误'));
+ return;
+ }
+ }
+
+ API.toast('保存成功');
+ modal.remove();
+ refreshRepairsList();
} catch(e) {
API.toast('保存失败: ' + (e.message || '网络错误'));
}
@@ -1377,6 +1568,64 @@ const Upload = (() => {
updateSelectedCount();
});
+ // 添加快递单号按钮
+ const trackingBtn = document.getElementById('repairs-tracking-btn');
+ addListener(trackingBtn, 'click', async () => {
+ if(selectedIds.size === 0) {
+ API.toast('请先勾选要添加快递单号的记录');
+ return;
+ }
+
+ // 创建弹窗
+ const modal = document.createElement('div');
+ modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000';
+ modal.innerHTML = `
+
+
+
添加快递单号
+
+
+
+
+ 已选择 ${selectedIds.size} 条记录
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ document.body.appendChild(modal);
+
+ document.getElementById('tracking-modal-close').onclick = () => modal.remove();
+ document.getElementById('tracking-cancel').onclick = () => modal.remove();
+ document.getElementById('tracking-submit').onclick = async () => {
+ const trackingNumber = document.getElementById('tracking-number-input').value.trim();
+ if(!trackingNumber) {
+ API.toast('请输入快递单号');
+ return;
+ }
+
+ try {
+ const result = await API.batchUpdateTracking(Array.from(selectedIds), trackingNumber);
+ if(result.ok) {
+ API.toast(`成功更新 ${result.updated} 条记录的快递单号`);
+ modal.remove();
+ refreshRepairsList();
+ } else {
+ API.toast(result.error || '更新失败');
+ }
+ } catch(e) {
+ API.toast('更新失败: ' + (e.message || '网络错误'));
+ }
+ };
+ });
+
// 导出按钮
const exportBtn = document.getElementById('repairs-export-btn');
addListener(exportBtn, 'click', async () => {
@@ -1437,17 +1686,18 @@ const Upload = (() => {
hasError = true;
}
+ // 验证不良现象
+ const defectSymptom = symptomInput.value.trim();
+ if(!defectSymptom) {
+ symptomError.textContent = '请输入不良现象';
+ symptomError.style.display = 'block';
+ hasError = true;
+ }
+
// 验证不良原因
- let defectReason = defectSelect.value;
- if(defectReason === '其他') {
- defectReason = defectOtherInput.value.trim();
- if(!defectReason) {
- defectError.textContent = '请输入其他原因';
- defectError.style.display = 'block';
- hasError = true;
- }
- } else if(!defectReason) {
- defectError.textContent = '请选择不良原因';
+ const defectReason = defectInput.value.trim();
+ if(!defectReason) {
+ defectError.textContent = '请输入不良原因';
defectError.style.display = 'block';
hasError = true;
}
@@ -1466,6 +1716,7 @@ const Upload = (() => {
const formData = new FormData();
formData.append('sn', sn);
formData.append('machine_type', machineType);
+ formData.append('defect_symptom', defectSymptom);
formData.append('defect_reason', defectReason);
formData.append('repair_method', methodInput.value.trim());
@@ -1487,9 +1738,8 @@ const Upload = (() => {
// 清空表单
snInput.value = '';
machineTypeSelect.value = '';
- defectSelect.value = '';
- defectOtherInput.value = '';
- defectOtherInput.style.display = 'none';
+ symptomInput.value = '';
+ defectInput.value = '';
methodInput.value = '';
methodGroup.style.opacity = '0.5';
methodInput.disabled = true;
diff --git a/frontend/js/router.js b/frontend/js/router.js
index bd9eddf..d977afd 100644
--- a/frontend/js/router.js
+++ b/frontend/js/router.js
@@ -54,6 +54,7 @@ const Router = (() => {
function updateBreadcrumb(path) {
const bc = document.getElementById('breadcrumb');
+ if (!bc) return; // 元素不存在时直接返回
const parts = path.split('/').filter(Boolean);
let acc = '#';
bc.innerHTML = parts.map((p, i) => {
diff --git a/server/app.py b/server/app.py
index ccd4a95..c63e463 100644
--- a/server/app.py
+++ b/server/app.py
@@ -152,6 +152,16 @@ def init_db():
c.execute('ALTER TABLE repair_records ADD COLUMN machine_type TEXT')
except:
pass
+ # 添加tracking_number字段(快递单号)
+ try:
+ c.execute('ALTER TABLE repair_records ADD COLUMN tracking_number TEXT')
+ except:
+ pass
+ # 添加defect_symptom字段(不良现象)
+ try:
+ c.execute('ALTER TABLE repair_records ADD COLUMN defect_symptom TEXT')
+ except:
+ pass
c.execute('''CREATE TABLE IF NOT EXISTS sop_files(
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
@@ -849,9 +859,8 @@ def dashboard():
# 计算不良率
bad_rate = "{}%".format(round((bad/(good+bad)) * 100, 2)) if (good+bad) > 0 else u'—'
- # 计算直通良品率(FPY = First Pass Yield)
- total_produced = good + bad
- fpy_rate = "{}%".format(round((fpy_good/total_produced) * 100, 2)) if total_produced > 0 else u'—'
+ # 直通良品数(FPY = First Pass Yield)
+ fpy_count = fpy_good
conn.close()
@@ -965,7 +974,7 @@ def dashboard():
shipments_count = (ship['total'] or 0) if ship else 0
return jsonify({
- 'fpyRate': fpy_rate,
+ 'fpyCount': fpy_count,
'goodRate': rate,
'badRate': bad_rate,
'shipments': shipments_count,
@@ -1370,7 +1379,7 @@ def upload_stats():
conn.commit()
conn.close()
- platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜'}.get(platform, platform)
+ platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
log('upload_stats', json.dumps({'good': good, 'bad': bad, 'fpy_good': fpy_good, 'platform': platform, 'details_count': len(details)}))
notify_superadmin('上传良/不良统计', f"平台: {platform_name}, 良品: {good}, 不良品: {bad}, 直通良品: {fpy_good}")
return jsonify({'ok': True})
@@ -1404,9 +1413,10 @@ def upload_repairs():
@require_login
@require_any_role('admin','superadmin')
def upload_repair_record():
- """上传返修记录(含设备SN、机种、不良原因、维修方法、图片)"""
+ """上传返修记录(含设备SN、机种、不良现象、不良原因、维修方法、图片)"""
sn = request.form.get('sn', '').strip()
machine_type = request.form.get('machine_type', '').strip()
+ defect_symptom = request.form.get('defect_symptom', '').strip()
defect_reason = request.form.get('defect_reason', '').strip()
repair_method = request.form.get('repair_method', '').strip()
@@ -1415,6 +1425,8 @@ def upload_repair_record():
return jsonify({'error': '设备SN为必填项'}), 400
if not machine_type:
return jsonify({'error': '机种为必填项'}), 400
+ if not defect_symptom:
+ return jsonify({'error': '不良现象为必填项'}), 400
if not defect_reason:
return jsonify({'error': '不良原因为必填项'}), 400
@@ -1464,14 +1476,14 @@ def upload_repair_record():
now = get_beijing_time()
images_json = json.dumps(image_filenames) if image_filenames else ''
- c.execute('INSERT INTO repair_records(sn, machine_type, defect_reason, repair_method, images, ts) VALUES(?,?,?,?,?,?)',
- (sn, machine_type, defect_reason, repair_method, images_json, now))
+ c.execute('INSERT INTO repair_records(sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts) VALUES(?,?,?,?,?,?,?)',
+ (sn, machine_type, defect_symptom, defect_reason, repair_method, images_json, now))
conn.commit()
record_id = c.lastrowid
conn.close()
- log('upload_repair_record', json.dumps({'sn': sn, 'machine_type': machine_type, 'defect_reason': defect_reason, 'images_count': len(image_filenames)}))
- notify_superadmin('上传返修记录', f"SN: {sn}, 机种: {machine_type}, 不良原因: {defect_reason}")
+ log('upload_repair_record', json.dumps({'sn': sn, 'machine_type': machine_type, 'defect_symptom': defect_symptom, 'defect_reason': defect_reason, 'images_count': len(image_filenames)}))
+ notify_superadmin('上传返修记录', f"SN: {sn}, 机种: {machine_type}, 不良现象: {defect_symptom}, 不良原因: {defect_reason}")
return jsonify({'ok': True, 'id': record_id})
@@ -1487,16 +1499,16 @@ def list_repair_records():
if sn_query:
# 支持模糊搜索
- c.execute('SELECT id, sn, machine_type, defect_reason, repair_method, images, ts FROM repair_records WHERE sn LIKE ? ORDER BY id DESC', (f'%{sn_query}%',))
+ c.execute('SELECT id, sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts, tracking_number FROM repair_records WHERE sn LIKE ? ORDER BY id DESC', (f'%{sn_query}%',))
elif limit:
try:
lim = int(limit)
- c.execute('SELECT id, sn, machine_type, defect_reason, repair_method, images, ts FROM repair_records ORDER BY id DESC LIMIT ?', (lim,))
+ c.execute('SELECT id, sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts, tracking_number FROM repair_records ORDER BY id DESC LIMIT ?', (lim,))
except:
- c.execute('SELECT id, sn, machine_type, defect_reason, repair_method, images, ts FROM repair_records ORDER BY id DESC')
+ c.execute('SELECT id, sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts, tracking_number FROM repair_records ORDER BY id DESC')
else:
# 不限制数量,返回所有记录
- c.execute('SELECT id, sn, machine_type, defect_reason, repair_method, images, ts FROM repair_records ORDER BY id DESC')
+ c.execute('SELECT id, sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts, tracking_number FROM repair_records ORDER BY id DESC')
rows = [dict(r) for r in c.fetchall()]
conn.close()
@@ -1549,7 +1561,7 @@ def export_repair_records():
# 获取选中的记录
placeholders = ','.join(['?' for _ in record_ids])
- c.execute(f'SELECT id, sn, machine_type, defect_reason, repair_method, images, ts FROM repair_records WHERE id IN ({placeholders}) ORDER BY id DESC', record_ids)
+ c.execute(f'SELECT id, sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts, tracking_number FROM repair_records WHERE id IN ({placeholders}) ORDER BY id DESC', record_ids)
rows = [dict(r) for r in c.fetchall()]
conn.close()
@@ -1564,7 +1576,7 @@ def export_repair_records():
from openpyxl.styles import Alignment
# 设置表头
- headers = ['序号', 'SN', '机种', '不良原因', '维修方法', '时间', '图片']
+ headers = ['序号', 'SN', '机种', '不良现象', '不良原因', '维修方法', '快递单号', '时间', '图片']
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.alignment = Alignment(horizontal='left', vertical='center')
@@ -1573,7 +1585,7 @@ def export_repair_records():
# 序号列宽度缩小
ws.column_dimensions['A'].width = 4.5
# 图片列宽度更大
- ws.column_dimensions['G'].width = 60
+ ws.column_dimensions['I'].width = 60
upload_dir = os.path.join(os.path.dirname(__file__), 'uploads', 'repair_images')
@@ -1588,16 +1600,20 @@ def export_repair_records():
cell2.alignment = data_alignment
cell3 = ws.cell(row=row_idx, column=3, value=record.get('machine_type') or '')
cell3.alignment = data_alignment
- cell4 = ws.cell(row=row_idx, column=4, value=record['defect_reason'])
+ cell4 = ws.cell(row=row_idx, column=4, value=record.get('defect_symptom') or '')
cell4.alignment = data_alignment
- cell5 = ws.cell(row=row_idx, column=5, value=record['repair_method'] or '')
+ cell5 = ws.cell(row=row_idx, column=5, value=record['defect_reason'])
cell5.alignment = data_alignment
+ cell6 = ws.cell(row=row_idx, column=6, value=record['repair_method'] or '')
+ cell6.alignment = data_alignment
+ cell7 = ws.cell(row=row_idx, column=7, value=record.get('tracking_number') or '')
+ cell7.alignment = data_alignment
# 格式化时间:去掉T和时区,只显示到秒
ts = record['ts']
if ts:
ts = ts.replace('T', ' ').split('.')[0].split('+')[0]
- cell6 = ws.cell(row=row_idx, column=6, value=ts)
- cell6.alignment = data_alignment
+ cell8 = ws.cell(row=row_idx, column=8, value=ts)
+ cell8.alignment = data_alignment
# 处理图片
images = []
@@ -1611,7 +1627,7 @@ def export_repair_records():
# 设置行高以容纳图片(增大到150像素)
ws.row_dimensions[row_idx].height = 150
- img_col = 7 # 图片列改为第7列
+ img_col = 9 # 图片列改为第9列
for img_idx, img_name in enumerate(images[:3]): # 最多显示3张图片
img_path = os.path.join(upload_dir, secure_filename(img_name))
if os.path.exists(img_path):
@@ -1634,7 +1650,7 @@ def export_repair_records():
else:
ws.cell(row=row_idx, column=img_col, value=f'图片不存在: {img_name}')
else:
- ws.cell(row=row_idx, column=7, value='无图片')
+ ws.cell(row=row_idx, column=9, value='无图片')
# 保存到内存
output = BytesIO()
@@ -1661,7 +1677,7 @@ def get_repair_record(record_id):
"""获取单条返修记录详情"""
conn = get_db()
c = conn.cursor()
- c.execute('SELECT id, sn, machine_type, defect_reason, repair_method, images, ts FROM repair_records WHERE id=?', (record_id,))
+ c.execute('SELECT id, sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts, tracking_number FROM repair_records WHERE id=?', (record_id,))
row = c.fetchone()
conn.close()
@@ -1688,6 +1704,7 @@ def update_repair_record(record_id):
data = request.get_json() or {}
sn = data.get('sn', '').strip()
machine_type = data.get('machine_type', '').strip()
+ defect_symptom = data.get('defect_symptom', '').strip()
defect_reason = data.get('defect_reason', '').strip()
repair_method = data.get('repair_method', '').strip()
@@ -1696,6 +1713,8 @@ def update_repair_record(record_id):
return jsonify({'error': '设备SN为必填项'}), 400
if not machine_type:
return jsonify({'error': '机种为必填项'}), 400
+ if not defect_symptom:
+ return jsonify({'error': '不良现象为必填项'}), 400
if not defect_reason:
return jsonify({'error': '不良原因为必填项'}), 400
@@ -1717,15 +1736,211 @@ def update_repair_record(record_id):
conn.close()
return jsonify({'error': '记录不存在'}), 404
- c.execute('UPDATE repair_records SET sn=?, machine_type=?, defect_reason=?, repair_method=? WHERE id=?',
- (sn, machine_type, defect_reason, repair_method, record_id))
+ # 获取快递单号(可选)
+ tracking_number = data.get('tracking_number', '').strip() if 'tracking_number' in data else None
+
+ if tracking_number is not None:
+ c.execute('UPDATE repair_records SET sn=?, machine_type=?, defect_symptom=?, defect_reason=?, repair_method=?, tracking_number=? WHERE id=?',
+ (sn, machine_type, defect_symptom, defect_reason, repair_method, tracking_number, record_id))
+ else:
+ c.execute('UPDATE repair_records SET sn=?, machine_type=?, defect_symptom=?, defect_reason=?, repair_method=? WHERE id=?',
+ (sn, machine_type, defect_symptom, defect_reason, repair_method, record_id))
conn.commit()
conn.close()
- log('update_repair_record', json.dumps({'id': record_id, 'sn': sn, 'defect_reason': defect_reason}))
+ log('update_repair_record', json.dumps({'id': record_id, 'sn': sn, 'defect_symptom': defect_symptom, 'defect_reason': defect_reason}))
return jsonify({'ok': True})
+@app.post('/api/repair-record/
/images')
+@require_login
+@require_any_role('admin','superadmin')
+def update_repair_images(record_id):
+ """更新返修记录的图片(支持添加新图片和删除现有图片)"""
+ conn = get_db()
+ c = conn.cursor()
+
+ # 检查记录是否存在
+ c.execute('SELECT images FROM repair_records WHERE id=?', (record_id,))
+ row = c.fetchone()
+ if not row:
+ conn.close()
+ return jsonify({'error': '记录不存在'}), 404
+
+ # 获取现有图片列表
+ existing_images = []
+ if row['images']:
+ try:
+ existing_images = json.loads(row['images'])
+ except:
+ existing_images = []
+
+ # 获取要保留的图片列表(从表单中)
+ keep_images = request.form.getlist('keep_images')
+
+ # 删除不再保留的图片文件
+ upload_dir = os.path.join(os.path.dirname(__file__), 'uploads', 'repair_images')
+ for img in existing_images:
+ if img not in keep_images:
+ img_path = os.path.join(upload_dir, secure_filename(img))
+ if os.path.exists(img_path):
+ try:
+ os.remove(img_path)
+ except:
+ pass
+
+ # 保留的图片
+ new_images = [img for img in keep_images if img in existing_images]
+
+ # 处理新上传的图片
+ os.makedirs(upload_dir, exist_ok=True)
+ files = request.files.getlist('new_images')
+
+ if len(new_images) + len(files) > 10:
+ conn.close()
+ return jsonify({'error': '最多保留10张图片'}), 400
+
+ for f in files:
+ if f and f.filename:
+ ext = f.filename.rsplit('.', 1)[-1].lower() if '.' in f.filename else ''
+ if ext not in ['jpg', 'jpeg', 'png', 'gif']:
+ conn.close()
+ return jsonify({'error': f'不支持的图片格式: {ext}'}), 400
+
+ f.seek(0, 2)
+ size = f.tell()
+ f.seek(0)
+ if size > 10 * 1024 * 1024:
+ conn.close()
+ return jsonify({'error': f'图片 {f.filename} 超过10MB限制'}), 400
+
+ import time
+ timestamp = int(time.time() * 1000)
+ filename = f"{timestamp}_{secure_filename(f.filename)}"
+ filepath = os.path.join(upload_dir, filename)
+ f.save(filepath)
+ new_images.append(filename)
+
+ # 更新数据库
+ images_json = json.dumps(new_images) if new_images else ''
+ c.execute('UPDATE repair_records SET images=? WHERE id=?', (images_json, record_id))
+ conn.commit()
+ conn.close()
+
+ log('update_repair_images', json.dumps({'id': record_id, 'images_count': len(new_images)}))
+ return jsonify({'ok': True, 'images': new_images})
+
+
+@app.post('/api/repair-records/batch-tracking')
+@require_login
+@require_any_role('admin','superadmin')
+def batch_update_tracking():
+ """批量更新返修记录的快递单号"""
+ data = request.get_json() or {}
+ record_ids = data.get('ids', [])
+ tracking_number = data.get('tracking_number', '').strip()
+
+ if not record_ids:
+ return jsonify({'error': '请选择要更新的记录'}), 400
+ if not tracking_number:
+ return jsonify({'error': '请输入快递单号'}), 400
+
+ conn = get_db()
+ c = conn.cursor()
+
+ placeholders = ','.join(['?' for _ in record_ids])
+ c.execute(f'UPDATE repair_records SET tracking_number=? WHERE id IN ({placeholders})',
+ [tracking_number] + record_ids)
+ updated_count = c.rowcount
+ conn.commit()
+ conn.close()
+
+ log('batch_update_tracking', json.dumps({'ids': record_ids, 'tracking_number': tracking_number}))
+ return jsonify({'ok': True, 'updated': updated_count})
+
+
+@app.get('/api/tracking/')
+@require_login
+def query_tracking(tracking_number):
+ """查询快递物流信息"""
+ import hashlib
+ import urllib.request
+ import urllib.parse
+
+ # 快递100 API配置
+ key = 'TsoMoTqO4243'
+ customer = 'EE2065B5BA6BE5ECEF52EB64F604532B'
+ url = 'https://poll.kuaidi100.com/poll/query.do'
+
+ # 根据单号前缀自动识别快递公司
+ tracking_number = tracking_number.strip().upper()
+ com = 'auto' # 默认自动识别
+
+ # 常见快递公司单号前缀
+ if tracking_number.startswith('SF'):
+ com = 'shunfeng'
+ elif tracking_number.startswith('YT'):
+ com = 'yuantong'
+ elif tracking_number.startswith('ZT'):
+ com = 'zhongtong'
+ elif tracking_number.startswith('YD'):
+ com = 'yunda'
+ elif tracking_number.startswith('JD') or tracking_number.startswith('JDVB'):
+ com = 'jd'
+ elif tracking_number.startswith('EMS') or tracking_number.startswith('10'):
+ com = 'ems'
+ elif tracking_number.startswith('STO'):
+ com = 'shentong'
+ elif tracking_number.startswith('DBL'):
+ com = 'debangkuaidi'
+
+ param = {
+ 'com': com,
+ 'num': tracking_number,
+ 'phone': '',
+ 'from': '',
+ 'to': '',
+ 'resultv2': '1',
+ 'show': '0',
+ 'order': 'desc'
+ }
+ param_str = json.dumps(param)
+
+ # 签名加密
+ temp_sign = param_str + key + customer
+ md = hashlib.md5()
+ md.update(temp_sign.encode())
+ sign = md.hexdigest().upper()
+
+ request_data = urllib.parse.urlencode({'customer': customer, 'param': param_str, 'sign': sign}).encode('utf-8')
+
+ try:
+ req = urllib.request.Request(url, data=request_data, method='POST')
+ req.add_header('Content-Type', 'application/x-www-form-urlencoded')
+ with urllib.request.urlopen(req, timeout=10) as response:
+ result = json.loads(response.read().decode('utf-8'))
+
+ # 快递100返回格式: {"message":"ok","state":"3","status":"200","condition":"F00","ischeck":"1","com":"jd","nu":"JDVB42605717549","data":[...]}
+ if result.get('status') == '200' or result.get('message') == 'ok':
+ return jsonify({
+ 'ok': True,
+ 'com': result.get('com', com),
+ 'nu': result.get('nu', tracking_number),
+ 'state': result.get('state', '0'),
+ 'ischeck': result.get('ischeck', '0'),
+ 'data': result.get('data', [])
+ })
+ else:
+ return jsonify({
+ 'ok': False,
+ 'error': result.get('message', '查询失败'),
+ 'returnCode': result.get('returnCode', '')
+ })
+ except Exception as e:
+ log('tracking_query_error', str(e))
+ return jsonify({'ok': False, 'error': f'查询失败: {str(e)}'}), 500
+
+
@app.delete('/api/repair-record/')
@require_login
@require_any_role('superadmin')
@@ -1817,7 +2032,7 @@ def upload_shipments():
conn.commit()
conn.close()
- platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜'}.get(platform, platform)
+ platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
log_data = {'date': date, 'qty': qty, 'to': to, 'platform': platform}
if box_no:
log_data['box_no'] = box_no
@@ -2893,7 +3108,7 @@ def query_shipment_by_sn():
# 解析 JSON 数据
shipment_info = json.loads(result)
platform = shipment_info.get('platform', 'pdd') # 默认拼多多
- platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜'}.get(platform, platform)
+ platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
return jsonify({
'found': True,
'sn': sn,
@@ -2938,7 +3153,7 @@ 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': '兔喜'}.get(platform, platform)
+ platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
matched_records.append({
'sn': sn.decode('utf-8') if isinstance(sn, bytes) else sn,
'date': shipment_info.get('date'),
diff --git a/server/synquery.py b/server/synquery.py
new file mode 100644
index 0000000..8b2c67f
--- /dev/null
+++ b/server/synquery.py
@@ -0,0 +1,46 @@
+# coding = utf-8
+import hashlib
+import json
+
+import requests
+
+
+class KuaiDi100:
+ def __init__(self):
+ self.key = '' # TODO 客户授权key
+ self.customer = '' # TODO 查询公司编号
+ self.url = 'https://poll.kuaidi100.com/poll/query.do' # 请求地址
+
+ def track(self, com, num, phone, ship_from, ship_to):
+ """
+ 物流轨迹实时查询
+ :param com: 查询的快递公司的编码,一律用小写字母
+ :param num: 查询的快递单号,单号的最大长度是32个字符
+ :param phone: 收件人或寄件人的手机号或固话(也可以填写后四位,如果是固话,请不要上传分机号)
+ :param ship_from: 出发地城市,省-市-区,非必填,填了有助于提升签收状态的判断的准确率,请尽量提供
+ :param ship_to: 目的地城市,省-市-区,非必填,填了有助于提升签收状态的判断的准确率,且到达目的地后会加大监控频率,请尽量提供
+ :return: requests.Response.text
+ """
+ param = {
+ 'com': com,
+ 'num': num,
+ 'phone': phone,
+ 'from': ship_from,
+ 'to': ship_to,
+ 'resultv2': '1', # 添加此字段表示开通行政区域解析功能。0:关闭(默认),1:开通行政区域解析功能,2:开通行政解析功能并且返回出发、目的及当前城市信息
+ 'show': '0', # 返回数据格式。0:json(默认),1:xml,2:html,3:text
+ 'order': 'desc' # 返回结果排序方式。desc:降序(默认),asc:升序
+ }
+ param_str = json.dumps(param) # 转json字符串
+
+ # 签名加密, 用于验证身份, 按param + key + customer 的顺序进行MD5加密(注意加密后字符串要转大写), 不需要“+”号
+ temp_sign = param_str + self.key + self.customer
+ md = hashlib.md5()
+ md.update(temp_sign.encode())
+ sign = md.hexdigest().upper()
+ request_data = {'customer': self.customer, 'param': param_str, 'sign': sign}
+ return requests.post(self.url, request_data).text # 发送请求
+
+
+result = KuaiDi100().track('yuantong', 'YT9693083639795', '', '广东省江门市', '广东省深圳市')
+print(result)
diff --git a/server/uploads/repair_images/1765519655516_5e3d7696-81a5-412d-ad1e-699921618764.jpg b/server/uploads/repair_images/1765519655516_5e3d7696-81a5-412d-ad1e-699921618764.jpg
new file mode 100644
index 0000000..82768b2
Binary files /dev/null and b/server/uploads/repair_images/1765519655516_5e3d7696-81a5-412d-ad1e-699921618764.jpg differ
diff --git a/server/uploads/repair_images/1765521258711_png b/server/uploads/repair_images/1765521258711_png
new file mode 100644
index 0000000..700031a
Binary files /dev/null and b/server/uploads/repair_images/1765521258711_png differ
diff --git a/server/uploads/repair_images/1765522104011_1.png b/server/uploads/repair_images/1765522104011_1.png
new file mode 100644
index 0000000..69f0067
Binary files /dev/null and b/server/uploads/repair_images/1765522104011_1.png differ
diff --git a/server/uploads/repair_images/1765525223925_1.png b/server/uploads/repair_images/1765525223925_1.png
new file mode 100644
index 0000000..69f0067
Binary files /dev/null and b/server/uploads/repair_images/1765525223925_1.png differ
diff --git a/server/uploads/repair_images/1765526570241_20251210153156_81_205.jpg b/server/uploads/repair_images/1765526570241_20251210153156_81_205.jpg
new file mode 100644
index 0000000..f096cf8
Binary files /dev/null and b/server/uploads/repair_images/1765526570241_20251210153156_81_205.jpg differ
diff --git a/server/uploads/repair_images/1765528878210_IMG_5165.jpeg b/server/uploads/repair_images/1765528878210_IMG_5165.jpeg
new file mode 100644
index 0000000..7cfbb29
Binary files /dev/null and b/server/uploads/repair_images/1765528878210_IMG_5165.jpeg differ