diff --git a/frontend/assets/icon-tuxi.png b/frontend/assets/icon-tuxi.png new file mode 100644 index 0000000..449642d Binary files /dev/null and b/frontend/assets/icon-tuxi.png differ diff --git a/frontend/js/api.js b/frontend/js/api.js index 035833f..ea322b9 100644 --- a/frontend/js/api.js +++ b/frontend/js/api.js @@ -173,8 +173,12 @@ const API = (() => { listShipments: () => request('/list/shipments'), auditPdd: (params={}) => request('/audit/pdd' + buildQuery(params)), auditYt: (params={}) => request('/audit/yt' + buildQuery(params)), + auditTx: (params={}) => request('/audit/tx' + buildQuery(params)), + auditMt: (params={}) => request('/audit/mt' + buildQuery(params)), auditPddQuiet: (params={}) => requestQuiet('/audit/pdd' + buildQuery(params)), auditYtQuiet: (params={}) => requestQuiet('/audit/yt' + buildQuery(params)), + auditTxQuiet: (params={}) => requestQuiet('/audit/tx' + buildQuery(params)), + auditMtQuiet: (params={}) => requestQuiet('/audit/mt' + buildQuery(params)), exportExcel: params => request('/export/excel', { method: 'POST', body: JSON.stringify(params) }), exportPdf: params => request('/export/pdf', { method: 'POST', body: JSON.stringify(params) }), toast, diff --git a/frontend/js/components/dashboard.js b/frontend/js/components/dashboard.js index 2e3dde3..8a94cfd 100644 --- a/frontend/js/components/dashboard.js +++ b/frontend/js/components/dashboard.js @@ -67,8 +67,6 @@ const Dashboard = (() => { ${badRate}
- ${trendValue} - 较昨日
@@ -143,8 +141,6 @@ const Dashboard = (() => { pcs
- ${trendValue} - 较昨日
@@ -182,7 +178,7 @@ const Dashboard = (() => { if (title === '良品 / 不良率') { // 良品率趋势(简化处理,显示固定趋势) - return { value: "+2.4%", color: "#10b981", text: "较昨日" }; + return { value: "", color: "#10b981", text: "" }; } else if (title.includes('今日产量')) { // 获取平台特定产量 const platform = title.includes('拼多多') ? 'pdd' : 'yt'; @@ -198,7 +194,7 @@ const Dashboard = (() => { // 计算趋势 if (yesterdayValue === 0) { - return { value: todayValue > 0 ? "+100%" : "0%", color: "#10b981", text: "较昨日" }; + return { value: "", color: "#10b981", text: "" }; } const change = ((todayValue - yesterdayValue) / yesterdayValue * 100).toFixed(1); @@ -211,9 +207,9 @@ const Dashboard = (() => { } return { - value: (isUp ? "+" : "") + change + "%", + value: "", color: color, - text: "较昨日" + text: "" }; }; @@ -245,8 +241,6 @@ const Dashboard = (() => { pcs
- ${trendValue} - ${trendText}
@@ -476,14 +470,18 @@ const Dashboard = (() => { }); async function render() { - const [dRes,pRes,yRes] = await Promise.allSettled([ + const [dRes,pRes,yRes,tRes,mRes] = await Promise.allSettled([ API.dashboard(), API.auditPddQuiet(), - API.auditYtQuiet() + API.auditYtQuiet(), + API.auditTxQuiet(), + API.auditMtQuiet() ]); const data = dRes.status==='fulfilled' ? dRes.value : { goodRate: '—', shipments: '—', defects: '—', badCount: '—' }; const pdd = pRes.status==='fulfilled' ? pRes.value : { list: [] }; const yt = yRes.status==='fulfilled' ? yRes.value : { list: [] }; + const tx = tRes.status==='fulfilled' ? tRes.value : { list: [] }; + const mt = mRes.status==='fulfilled' ? mRes.value : { list: [] }; // 优化:默认只显示少量数据,完整数据在点击模态框时加载 const pddList = (pdd.list||[]).slice(0, 10).map(r=>`
  • ${r.ts_cn||'—'}${r.batch||''}${r.mac||''}${r.note||''}
  • `).join('')||'
  • 暂无数据
  • '; @@ -514,6 +512,8 @@ const Dashboard = (() => { window.__pddParams = window.__pddParams || {}; window.__ytParams = window.__ytParams || {}; + window.__txParams = window.__txParams || {}; + window.__mtParams = window.__mtParams || {}; window.__auditBusy=false; const toEpoch=(s)=>{try{if(!s)return null;let t=s.trim(); if(/Z$/.test(t) || /[\+\-]\d{2}:\d{2}$/.test(t)) return Date.parse(t); @@ -551,7 +551,7 @@ const Dashboard = (() => { // 绘制趋势图 let chartData = null; - const drawTrendChart = (pddData, ytData) => { + const drawTrendChart = (pddData, ytData, txData, mtData) => { const canvas = document.getElementById('trend-chart'); if(!canvas) return; const ctx = canvas.getContext('2d'); @@ -639,10 +639,15 @@ const Dashboard = (() => { const pddCounts = countByTime(pddData); const ytCounts = countByTime(ytData); - const maxCount = Math.max(...pddCounts, ...ytCounts, 1); - - // 绘制参数(使用逻辑尺寸而非物理像素) - // 根据最大值动态调整左边距,确保Y轴标签不被截断 + const txCounts = countByTime(txData); + const mtCounts = countByTime(mtData); + const actualMax = Math.max(...pddCounts, ...ytCounts, ...txCounts, ...mtCounts, 0); + // 动态调整Y轴:让小数据更明显 + let maxCount; + if (actualMax <= 2) maxCount = 3; // 数据是1-2时,Y轴最大值设为3,让数据占据1/3到2/3的高度 + else if (actualMax <= 5) maxCount = 8; // 数据是3-5时,Y轴最大值设为8 + else if (actualMax <= 10) maxCount = 15; // 数据是6-10时,Y轴最大值设为15 + else maxCount = actualMax; const maxDigits = Math.max(maxCount.toString().length, 2); const dynamicLeftPadding = Math.max(50, 25 + maxDigits * 10); const padding = {left: dynamicLeftPadding, right: 20, top: 20, bottom: 30}; @@ -654,7 +659,7 @@ const Dashboard = (() => { const effectiveChartHeight = chartHeight - yAxisMargin; // 保存图表数据供鼠标事件使用 - chartData = {timePoints, pddCounts, ytCounts, maxCount, padding, chartWidth, chartHeight, timeRange, yAxisMargin, effectiveChartHeight}; + chartData = {timePoints, pddCounts, ytCounts, txCounts, mtCounts, maxCount, padding, chartWidth, chartHeight, timeRange, yAxisMargin, effectiveChartHeight}; // 清空画布 ctx.fillStyle = colors.bg; @@ -751,9 +756,13 @@ const Dashboard = (() => { // 收集今日标签位置 const todayLabels = []; const drawAreaLine = (counts, gradientColors, fillColors, labelKey) => { + // 为每个平台单独计算最大值 + const platformMax = Math.max(...counts, 1); + const platformScale = platformMax <= 2 ? 10 : (platformMax <= 5 ? 15 : platformMax); + const points = counts.map((count, i) => ({ x: padding.left + (chartWidth / (timePoints.length - 1)) * i, - y: padding.top + chartHeight - yAxisMargin - (count / maxCount) * effectiveChartHeight + y: padding.top + chartHeight - yAxisMargin - (count / platformScale) * effectiveChartHeight })); // 1. 绘制填充区域 @@ -837,8 +846,10 @@ const Dashboard = (() => { }); }; - // 先绘制圆通(紫色在下层),再绘制拼多多(橙色在上层) + // 绘制4条曲线:圆通、美团、兔喜、拼多多(从下到上) drawAreaLine(ytCounts, colors.ytGradient, colors.ytFill, 'yt'); + drawAreaLine(mtCounts, ['rgba(255, 184, 0, 0.8)', 'rgba(255, 184, 0, 1)', 'rgba(255, 200, 50, 1)'], ['rgba(255, 184, 0, 0.25)', 'rgba(255, 184, 0, 0.05)'], 'mt'); + drawAreaLine(txCounts, ['rgba(255, 107, 129, 0.8)', 'rgba(255, 107, 129, 1)', 'rgba(255, 130, 150, 1)'], ['rgba(255, 107, 129, 0.25)', 'rgba(255, 107, 129, 0.05)'], 'tx'); drawAreaLine(pddCounts, colors.pddGradient, colors.pddFill, 'pdd'); // 智能绘制今日标签,显示在高亮点的左边(数字为0时不显示) @@ -879,12 +890,14 @@ const Dashboard = (() => { }; // 初始化缓存数据 - window.__auditCache = window.__auditCache || {pdd: [], yt: []}; + window.__auditCache = window.__auditCache || {pdd: [], yt: [], tx: [], mt: []}; if(pdd.list && pdd.list.length > 0) window.__auditCache.pdd = pdd.list; if(yt.list && yt.list.length > 0) window.__auditCache.yt = yt.list; + if(tx.list && tx.list.length > 0) window.__auditCache.tx = tx.list; + if(mt.list && mt.list.length > 0) window.__auditCache.mt = mt.list; // 初始绘制 - drawTrendChart(pdd.list, yt.list); + drawTrendChart(pdd.list, yt.list, tx.list, mt.list); // 监听主题切换事件,立即重绘图表 const themeChangeHandler = () => { @@ -895,7 +908,13 @@ const Dashboard = (() => { const ytData = (window.__auditCache && window.__auditCache.yt && window.__auditCache.yt.length > 0) ? window.__auditCache.yt : yt.list; - drawTrendChart(pddData, ytData); + const txData = (window.__auditCache && window.__auditCache.tx && window.__auditCache.tx.length > 0) + ? window.__auditCache.tx + : tx.list; + const mtData = (window.__auditCache && window.__auditCache.mt && window.__auditCache.mt.length > 0) + ? window.__auditCache.mt + : mt.list; + drawTrendChart(pddData, ytData, txData, mtData); }; window.addEventListener('themeChanged', themeChangeHandler); @@ -914,7 +933,7 @@ const Dashboard = (() => { const rect = canvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; - const {timePoints, pddCounts, ytCounts, maxCount, padding, chartWidth, chartHeight, timeRange, yAxisMargin, effectiveChartHeight} = chartData; + const {timePoints, pddCounts, ytCounts, txCounts, mtCounts, maxCount, padding, chartWidth, chartHeight, timeRange, yAxisMargin, effectiveChartHeight} = chartData; // 查找最近的数据点 let nearestIndex = -1; @@ -923,9 +942,13 @@ const Dashboard = (() => { const x = padding.left + (chartWidth / (timePoints.length - 1)) * i; const pddY = padding.top + chartHeight - yAxisMargin - (pddCounts[i] / maxCount) * effectiveChartHeight; const ytY = padding.top + chartHeight - yAxisMargin - (ytCounts[i] / maxCount) * effectiveChartHeight; + const txY = padding.top + chartHeight - yAxisMargin - (txCounts[i] / maxCount) * effectiveChartHeight; + const mtY = padding.top + chartHeight - yAxisMargin - (mtCounts[i] / maxCount) * effectiveChartHeight; const distPdd = Math.sqrt((mouseX - x) ** 2 + (mouseY - pddY) ** 2); const distYt = Math.sqrt((mouseX - x) ** 2 + (mouseY - ytY) ** 2); - const dist = Math.min(distPdd, distYt); + const distTx = Math.sqrt((mouseX - x) ** 2 + (mouseY - txY) ** 2); + const distMt = Math.sqrt((mouseX - x) ** 2 + (mouseY - mtY) ** 2); + const dist = Math.min(distPdd, distYt, distTx, distMt); if(dist < minDist){ minDist = dist; nearestIndex = i; @@ -936,10 +959,12 @@ const Dashboard = (() => { const point = timePoints[nearestIndex]; const pddCount = pddCounts[nearestIndex]; const ytCount = ytCounts[nearestIndex]; + const txCount = txCounts[nearestIndex]; + const mtCount = mtCounts[nearestIndex]; let label = point; if(timeRange === 'week') label = point + ' 周'; else if(timeRange === 'month') label = point + ' 月'; - tooltip.innerHTML = `
    ${label}
    拼多多: ${pddCount}
    圆通: ${ytCount}
    `; + tooltip.innerHTML = `
    ${label}
    拼多多: ${pddCount}
    兔喜: ${txCount}
    美团: ${mtCount}
    圆通: ${ytCount}
    `; tooltip.style.display = 'block'; // 获取提示框宽度以便放在鼠标左侧 @@ -973,8 +998,10 @@ const Dashboard = (() => { window.__trendTimeRange = tab.dataset.range; const pddData = window.__auditCache?.pdd || pdd.list; const ytData = window.__auditCache?.yt || yt.list; - drawTrendChart(pddData, ytData); - updateTrendStats(pddData, ytData); + const txData = window.__auditCache?.tx || tx.list; + const mtData = window.__auditCache?.mt || mt.list; + drawTrendChart(pddData, ytData, txData, mtData); + updateTrendStats(pddData, ytData, txData, mtData); }; }); // 初始化active样式 @@ -986,7 +1013,7 @@ const Dashboard = (() => { } // 更新趋势统计数据 - const updateTrendStats = (pddData, ytData) => { + const updateTrendStats = (pddData, ytData, txData, mtData) => { const now = new Date(); const today = now.toISOString().split('T')[0]; const thisMonth = today.slice(0, 7); @@ -1029,6 +1056,8 @@ const Dashboard = (() => { // 保存今日各平台产量 history[today].pdd = countTodayUnique(pddData); history[today].yt = countTodayUnique(ytData); + history[today].tx = countTodayUnique(txData); + history[today].mt = countTodayUnique(mtData); // 计算并保存不良数量(假设不良数据在某个字段中) // 这里需要根据实际数据结构调整 @@ -1057,9 +1086,13 @@ const Dashboard = (() => { const pddMonth = countMonthUnique(pddData); const ytMonth = countMonthUnique(ytData); + const txMonth = countMonthUnique(txData); + const mtMonth = countMonthUnique(mtData); const pddToday = countTodayUnique(pddData); const ytToday = countTodayUnique(ytData); - const weekTotal = countWeekUnique(pddData) + countWeekUnique(ytData); + const txToday = countTodayUnique(txData); + const mtToday = countTodayUnique(mtData); + const weekTotal = countWeekUnique(pddData) + countWeekUnique(ytData) + countWeekUnique(txData) + countWeekUnique(mtData); // 更新DOM const el = (id) => document.getElementById(id); @@ -1067,12 +1100,14 @@ const Dashboard = (() => { if(el('trend-total-yt')) el('trend-total-yt').textContent = ytMonth.toLocaleString(); if(el('trend-today-pdd')) el('trend-today-pdd').textContent = pddToday.toLocaleString(); if(el('trend-today-yt')) el('trend-today-yt').textContent = ytToday.toLocaleString(); + if(el('trend-today-tx')) el('trend-today-tx').textContent = txToday.toLocaleString(); + if(el('trend-today-mt')) el('trend-today-mt').textContent = mtToday.toLocaleString(); if(el('trend-week-total')) el('trend-week-total').textContent = weekTotal.toLocaleString(); // 状态判断 const statusEl = el('trend-status'); if(statusEl){ - if(pddToday + ytToday > 0){ + if(pddToday + ytToday + txToday + mtToday > 0){ statusEl.textContent = '生产中'; statusEl.style.color = '#10b981'; } else { @@ -1706,16 +1741,33 @@ const Dashboard = (() => { // 更新整合看板显示 const updateAuditDisplay = () => { const platform = window.__currentAuditPlatform; - const params = platform === 'pdd' ? window.__pddParams : window.__ytParams; + let params, cacheData, platformIcon; + + if (platform === 'pdd') { + params = window.__pddParams; + cacheData = window.__auditCache.pdd; + platformIcon = ''; + } else if (platform === 'yt') { + params = window.__ytParams; + cacheData = window.__auditCache.yt; + platformIcon = ''; + } else if (platform === 'tx') { + params = window.__txParams || {}; + cacheData = window.__auditCache.tx; + platformIcon = ''; + } else if (platform === 'mt') { + params = window.__mtParams || {}; + cacheData = window.__auditCache.mt; + platformIcon = ''; + } + const sTime = toEpoch(params.start); const eTime = toEpoch(params.end); - const cacheData = platform === 'pdd' ? window.__auditCache.pdd : window.__auditCache.yt; const filteredData = (cacheData || []).filter(r => { const t = toEpoch(r.ts_cn); if (t == null) return false; return (sTime == null || t >= sTime) && (eTime == null || t <= eTime); }); - const platformIcon = platform === 'pdd' ? '' : ''; const listHtml = filteredData.slice(0, 100).map(r => `
  • ${platformIcon}${r.ts_cn||'—'}${r.batch||''}${r.mac||''}${r.note||''}
  • ` ).join('') || '
  • 暂无数据
  • '; @@ -1726,10 +1778,9 @@ const Dashboard = (() => { if (platformSelect) { platformSelect.onchange = () => { window.__currentAuditPlatform = platformSelect.value; - // 同步日期选择器 - const otherDateEl = platformSelect.value === 'pdd' ? q('audit-date-pdd') : q('audit-date-yt'); - if (auditDateEl && otherDateEl) { - auditDateEl.value = otherDateEl.value; + // 清空日期选择器 + if (auditDateEl) { + auditDateEl.value = ''; } updateAuditDisplay(); }; @@ -1742,14 +1793,13 @@ const Dashboard = (() => { const platform = window.__currentAuditPlatform; if (platform === 'pdd') { window.__pddParams = dateToRange(d); - const pddDateEl = q('audit-date-pdd'); - if (pddDateEl) pddDateEl.value = d; - } else { + } else if (platform === 'yt') { window.__ytParams = dateToRange(d); - const ytDateEl = q('audit-date-yt'); - if (ytDateEl) ytDateEl.value = d; + } else if (platform === 'tx') { + window.__txParams = dateToRange(d); + } else if (platform === 'mt') { + window.__mtParams = dateToRange(d); } - refreshAuditLists(); updateAuditDisplay(); }; } @@ -1812,7 +1862,7 @@ const Dashboard = (() => { try { // 请求全部数据(不限制),传递AbortController信号 const signal = window.__auditAbortController.signal; - const [pddRes, ytRes, dashRes] = await Promise.all([ + const [pddRes, ytRes, txRes, mtRes, dashRes] = await Promise.all([ fetch('/api/audit/pdd', { headers: { 'Content-Type': 'application/json' }, credentials: 'include', @@ -1829,6 +1879,22 @@ const Dashboard = (() => { if(e.name === 'AbortError') console.log('[Dashboard] YT请求被取消'); return {list:[]}; }), + fetch('/api/audit/tx', { + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + signal + }).then(r => r.ok ? r.json() : {list:[]}).catch((e)=>{ + if(e.name === 'AbortError') console.log('[Dashboard] TX请求被取消'); + return {list:[]}; + }), + fetch('/api/audit/mt', { + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + signal + }).then(r => r.ok ? r.json() : {list:[]}).catch((e)=>{ + if(e.name === 'AbortError') console.log('[Dashboard] MT请求被取消'); + return {list:[]}; + }), fetch('/api/dashboard', { headers: { 'Content-Type': 'application/json' }, credentials: 'include', @@ -1896,10 +1962,12 @@ const Dashboard = (() => { // 缓存全部数据(不再限制30天,支持大数据量显示) window.__auditCache.pdd = pddRes.list || []; window.__auditCache.yt = ytRes.list || []; + window.__auditCache.tx = txRes.list || []; + window.__auditCache.mt = mtRes.list || []; // 更新趋势图和统计数据 - drawTrendChart(window.__auditCache.pdd, window.__auditCache.yt); - updateTrendStats(window.__auditCache.pdd, window.__auditCache.yt); + drawTrendChart(window.__auditCache.pdd, window.__auditCache.yt, window.__auditCache.tx, window.__auditCache.mt); + updateTrendStats(window.__auditCache.pdd, window.__auditCache.yt, window.__auditCache.tx, window.__auditCache.mt); refreshShipmentPlatformStats(false); // 更新列表(应用筛选,只显示前100条) @@ -1990,7 +2058,7 @@ const Dashboard = (() => {
    -
    +
    @@ -2019,8 +2087,10 @@ const Dashboard = (() => {
    - 圆通 拼多多 + 兔喜 + 美团 + 圆通
    @@ -2030,18 +2100,26 @@ const Dashboard = (() => {
    -
    +
    -
    +
    今日拼多多
    0
    +
    +
    今日兔喜
    +
    0
    +
    +
    +
    今日美团
    +
    0
    +
    今日圆通
    0
    @@ -2057,7 +2135,7 @@ const Dashboard = (() => {
    -
    +
    产量占比
    @@ -2101,6 +2179,8 @@ const Dashboard = (() => {
    diff --git a/server/app.py b/server/app.py index cdfd8b7..60eca4e 100644 --- a/server/app.py +++ b/server/app.py @@ -12,7 +12,7 @@ try: except Exception: redis = None _redis_client = None -_audit_cache = {'pdd': {'ts': 0, 'list': []}, 'yt': {'ts': 0, 'list': []}} +_audit_cache = {'pdd': {'ts': 0, 'list': []}, 'yt': {'ts': 0, 'list': []}, 'tx': {'ts': 0, 'list': []}, 'mt': {'ts': 0, 'list': []}} BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.path.join(BASE_DIR, 'data.db') @@ -1265,13 +1265,217 @@ def audit_yt(): return jsonify({'list': []}) +@app.get('/api/audit/tx') +@require_login +def audit_tx(): + start = datetime.utcnow() + try: + q_start = request.args.get('start') + q_end = request.args.get('end') + q_limit = request.args.get('limit') + q_order = request.args.get('order', 'desc') + has_filter = bool(q_start or q_end or q_limit or q_order) + + # 缓存优化:3秒内不重复查询 + if (not has_filter) and ((datetime.utcnow().timestamp() - _audit_cache['tx']['ts']) < 3): + return jsonify({'list': _audit_cache['tx']['list']}) + + r = get_redis() + # 设置Redis超时为5秒 + r.connection_pool.connection_kwargs['socket_timeout'] = 5 + + items = [] + # 支持大数据量:默认返回全部数据,可通过limit参数限制 + max_items = int(q_limit) if q_limit else 50000 + + for key in ['mac_batch_audit_tx', 'audit:tx', 'tx:audit']: + try: + if r.exists(key): + t = r.type(key) + if t == 'list': + # 获取全部数据或限制数量 + total = r.llen(key) + fetch_count = min(total, max_items) + items = r.lrange(key, -fetch_count, -1) + elif t == 'zset': + items = r.zrevrange(key, 0, max_items - 1) + elif t == 'stream': + entries = r.xrevrange(key, max='+', min='-', count=max_items) + items = [json.dumps(v) for _id, v in entries] + else: + v = r.get(key) + items = [v] if v else [] + break + except Exception as e: + log('audit_tx_error', f'Redis query error: {str(e)}') + continue + + res = [parse_audit_line(x) for x in items] + if q_start or q_end: + def to_epoch(s): + try: + if not s: + return None + if 'T' in s or 'Z' in s or '+' in s: + return datetime.fromisoformat(s.replace('Z','+00:00')).timestamp() + if ' ' in s and ':' in s: + return datetime.strptime(s, '%Y-%m-%d %H:%M:%S').timestamp() + return datetime.strptime(s, '%Y-%m-%d').timestamp() + except Exception: + return None + s_epoch = to_epoch(q_start) if q_start else None + e_epoch = to_epoch(q_end) if q_end else None + tmp = [] + for r0 in res: + ts = to_epoch(r0.get('ts_cn')) + if ts is None: + continue + if s_epoch is not None and ts < s_epoch: + continue + if e_epoch is not None and ts > e_epoch: + continue + tmp.append(r0) + res = tmp + try: + def to_key(r): + s = r.get('ts_cn') or '' + try: + if 'T' in s or 'Z' in s or '+' in s: + return datetime.fromisoformat(s.replace('Z','+00:00')).timestamp() + if ' ' in s and ':' in s: + return datetime.strptime(s, '%Y-%m-%d %H:%M:%S').timestamp() + return datetime.strptime(s, '%Y-%m-%d').timestamp() + except Exception: + return 0 + res.sort(key=to_key, reverse=(q_order != 'asc')) + except Exception: + res.reverse() + if q_limit: + try: + lim = int(q_limit) + if lim > 0: + res = res[:lim] + except Exception: + pass + if not has_filter: + _audit_cache['tx'] = {'ts': datetime.utcnow().timestamp(), 'list': res} + dur = (datetime.utcnow() - start).total_seconds() + log('audit_tx_cost', f"{dur}s len={len(res)}") + return jsonify({'list': res}) + except Exception as e: + log('audit_tx_error', str(e)) + return jsonify({'list': []}) + + +@app.get('/api/audit/mt') +@require_login +def audit_mt(): + start = datetime.utcnow() + try: + q_start = request.args.get('start') + q_end = request.args.get('end') + q_limit = request.args.get('limit') + q_order = request.args.get('order', 'desc') + has_filter = bool(q_start or q_end or q_limit or q_order) + + # 缓存优化:3秒内不重复查询 + if (not has_filter) and ((datetime.utcnow().timestamp() - _audit_cache['mt']['ts']) < 3): + return jsonify({'list': _audit_cache['mt']['list']}) + + r = get_redis() + # 设置Redis超时为5秒 + r.connection_pool.connection_kwargs['socket_timeout'] = 5 + + items = [] + # 支持大数据量:默认返回全部数据,可通过limit参数限制 + max_items = int(q_limit) if q_limit else 50000 + + for key in ['mac_batch_audit_mt', 'audit:mt', 'mt:audit']: + try: + if r.exists(key): + t = r.type(key) + if t == 'list': + # 获取全部数据或限制数量 + total = r.llen(key) + fetch_count = min(total, max_items) + items = r.lrange(key, -fetch_count, -1) + elif t == 'zset': + items = r.zrevrange(key, 0, max_items - 1) + elif t == 'stream': + entries = r.xrevrange(key, max='+', min='-', count=max_items) + items = [json.dumps(v) for _id, v in entries] + else: + v = r.get(key) + items = [v] if v else [] + break + except Exception as e: + log('audit_mt_error', f'Redis query error: {str(e)}') + continue + + res = [parse_audit_line(x) for x in items] + if q_start or q_end: + def to_epoch(s): + try: + if not s: + return None + if 'T' in s or 'Z' in s or '+' in s: + return datetime.fromisoformat(s.replace('Z','+00:00')).timestamp() + if ' ' in s and ':' in s: + return datetime.strptime(s, '%Y-%m-%d %H:%M:%S').timestamp() + return datetime.strptime(s, '%Y-%m-%d').timestamp() + except Exception: + return None + s_epoch = to_epoch(q_start) if q_start else None + e_epoch = to_epoch(q_end) if q_end else None + tmp = [] + for r0 in res: + ts = to_epoch(r0.get('ts_cn')) + if ts is None: + continue + if s_epoch is not None and ts < s_epoch: + continue + if e_epoch is not None and ts > e_epoch: + continue + tmp.append(r0) + res = tmp + try: + def to_key(r): + s = r.get('ts_cn') or '' + try: + if 'T' in s or 'Z' in s or '+' in s: + return datetime.fromisoformat(s.replace('Z','+00:00')).timestamp() + if ' ' in s and ':' in s: + return datetime.strptime(s, '%Y-%m-%d %H:%M:%S').timestamp() + return datetime.strptime(s, '%Y-%m-%d').timestamp() + except Exception: + return 0 + res.sort(key=to_key, reverse=(q_order != 'asc')) + except Exception: + res.reverse() + if q_limit: + try: + lim = int(q_limit) + if lim > 0: + res = res[:lim] + except Exception: + pass + if not has_filter: + _audit_cache['mt'] = {'ts': datetime.utcnow().timestamp(), 'list': res} + dur = (datetime.utcnow() - start).total_seconds() + log('audit_mt_cost', f"{dur}s len={len(res)}") + return jsonify({'list': res}) + except Exception as e: + log('audit_mt_error', str(e)) + return jsonify({'list': []}) + + @app.get('/api/audit/diagnose') @require_login def audit_diagnose(): try: r = get_redis() result = {} - for key in ['mac_batch_audit_pdd', 'mac_batch_audit_yt', 'batch_sn_mapping_pdd', 'batch_sn_mapping_yt']: + for key in ['mac_batch_audit_pdd', 'mac_batch_audit_yt', 'mac_batch_audit_tx', 'mac_batch_audit_mt', 'batch_sn_mapping_pdd', 'batch_sn_mapping_yt']: try: t = r.type(key) if t == 'list':