6.5冲刺

昨天的成就: 完成了AI识别历史记录功能的全栈开发,实现了Excel/PDF报告导出功能,编写并执行了团队培训,耗时10小时
遇到的困难: Excel图片插入定位复杂,PDF中文显示乱码,大批量导出内存溢出,现场演示API故障
今天的任务: Sprint 3功能验收,修复验收问题,编写总结报告,部署到测试环境
工作代码展示

  1. AI识别历史记录数据库模型 (backend/src/models/AIRecognitionLog.js)
    javascript
    const { DataTypes } = require('sequelize');
    const sequelize = require('../config/database');

const AIRecognitionLog = sequelize.define('AIRecognitionLog', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
applicationId: {
type: DataTypes.STRING,
allowNull: false,
comment: '关联申请ID'
},
stage: {
type: DataTypes.STRING,
comment: '施工阶段'
},
imagePath: {
type: DataTypes.STRING,
allowNull: false,
comment: '图片存储路径'
},
fileSize: {
type: DataTypes.BIGINT,
comment: '文件大小(字节)'
},
riskLevel: {
type: DataTypes.ENUM('low', 'medium', 'high'),
defaultValue: 'low',
comment: '风险等级'
},
safetyRisksCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '安全风险数量'
},
qualityIssuesCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '质量问题数量'
},
suggestionsCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '改进建议数量'
},
recognitionTime: {
type: DataTypes.INTEGER,
comment: '识别耗时(毫秒)'
},
success: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: '是否成功'
},
errorMessage: {
type: DataTypes.TEXT,
comment: '错误信息'
},
createdAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'ai_recognition_logs',
timestamps: false
});

module.exports = AIRecognitionLog;
2. Excel导出功能 (backend/src/services/exportService.js)
javascript
const ExcelJS = require('exceljs');
const fs = require('fs');
const path = require('path');

class ExportService {
/**

  • 导出AI识别记录为Excel
  • @param {Array} records - 识别记录数组
  • @param {string} outputPath - 输出文件路径
    */
    async exportToExcel(records, outputPath) {
    const workbook = new ExcelJS.Workbook();
    const worksheet = workbook.addWorksheet('AI识别记录');

// 定义列
worksheet.columns = [
{ header: '序号', key: 'id', width: 8 },
{ header: '图片', key: 'image', width: 35 },
{ header: '施工阶段', key: 'stage', width: 15 },
{ header: '风险等级', key: 'riskLevel', width: 12 },
{ header: '安全风险', key: 'risks', width: 40 },
{ header: '质量问题', key: 'quality', width: 40 },
{ header: '识别时间', key: 'time', width: 20 }
];

// 设置表头样式
const headerRow = worksheet.getRow(1);
headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
headerRow.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF4472C4' }
};
headerRow.height = 25;

// 分批处理记录(避免内存溢出)
const batchSize = 10;
for (let i = 0; i < records.length; i += batchSize) {
const batch = records.slice(i, i + batchSize);

for (const record of batch) {
const rowIndex = worksheet.rowCount + 1;
const row = worksheet.addRow({
id: i + 1,
stage: record.stage || '-',
riskLevel: this.getRiskLevelText(record.riskLevel),
risks: record.safetyRisks?.join('; ') || '无',
quality: record.qualityIssues?.join('; ') || '无',
time: new Date(record.createdAt).toLocaleString()
});

// 插入图片缩略图
if (record.imagePath && fs.existsSync(record.imagePath)) {
try {
const imageBuffer = fs.readFileSync(record.imagePath);
const imageId = workbook.addImage({
buffer: imageBuffer,
extension: 'jpeg'
});

worksheet.addImage(imageId, {
tl: { col: 1, row: rowIndex },
br: { col: 2, row: rowIndex },
editAs: 'oneCell'
});

row.height = 100; // 设置行高以容纳图片
} catch (err) {
console.error(插入图片失败: ${record.imagePath}, err);
}
}

// 交替行背景色
if (i % 2 === 0) {
row.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFF2F2F2' }
};
}
}
}

// 自动调整列宽
worksheet.columns.forEach(column => {
column.width = Math.max(column.width, 10);
});

// 流式写入文件
await workbook.xlsx.writeFile(outputPath);
console.log(Excel导出成功: ${outputPath});
}

getRiskLevelText(level) {
const map = { low: '低风险', medium: '中风险', high: '高风险' };
return map[level] || level;
}
}

module.exports = new ExportService();
3. PDF报告生成功能 (backend/src/services/pdfService.js)
javascript
const PDFDocument = require('pdfkit');
const fs = require('fs');
const path = require('path');

class PDFService {
constructor() {
// 注册中文字体(思源黑体)
this.fontPath = path.join(__dirname, '../fonts/SourceHanSansSC-Regular.otf');
}

/**

  • 生成AI识别PDF报告

  • @param {Array} records - 识别记录

  • @param {string} outputPath - 输出路径
    */
    async generatePDFReport(records, outputPath) {
    return new Promise((resolve, reject) => {
    const doc = new PDFDocument({
    size: 'A4',
    margin: 50,
    font: this.fontPath
    });

    const stream = fs.createWriteStream(outputPath);
    doc.pipe(stream);

    // 封面
    this.addCoverPage(doc, records.length);

    // 内容页
    records.forEach((record, index) => {
    if (index > 0) doc.addPage();
    this.addRecordPage(doc, record, index + 1);
    });

    // 页脚
    this.addFooter(doc, records.length);

    doc.end();

    stream.on('finish', () => {
    console.log(PDF报告生成成功: ${outputPath});
    resolve();
    });

    stream.on('error', reject);
    });
    }

addCoverPage(doc, count) {
doc.fontSize(24).font(this.fontPath).text('AI智能识别分析报告', { align: 'center' });
doc.moveDown(2);
doc.fontSize(16).text(生成日期: ${new Date().toLocaleDateString()}, { align: 'center' });
doc.text(识别记录总数: ${count} 条, { align: 'center' });
doc.moveDown(4);
doc.fontSize(12).text('本报告由 AI 图像识别系统自动生成', { align: 'center' });
}

addRecordPage(doc, record, pageNum) {
doc.fontSize(18).font(this.fontPath).text(记录 #${pageNum}: ${record.stage || '未知阶段'}, { underline: true });
doc.moveDown(1);

// 添加图片
if (record.imagePath && fs.existsSync(record.imagePath)) {
try {
doc.image(record.imagePath, 50, doc.y, { width: 200 });
} catch (err) {
doc.text('[图片加载失败]', 50, doc.y);
}
}

doc.moveDown(1);

// 右侧文本信息
doc.fontSize(12).font(this.fontPath);
doc.text(风险等级: ${this.getRiskLevelText(record.riskLevel)}, 270, doc.y - 150);
doc.text(识别耗时: ${record.recognitionTime}ms, 270, doc.y + 20);
doc.text(安全问题: ${record.safetyRisks?.length || 0} 项, 270, doc.y + 40);

doc.moveDown(2);
doc.text('详细分析:', { underline: true });
doc.moveDown(0.5);
doc.fontSize(10);
doc.text(- 安全风险: ${record.safetyRisks?.join(', ') || '无'});
doc.text(- 质量问题: ${record.qualityIssues?.join(', ') || '无'});
doc.text(- 改进建议: ${record.suggestions?.join(', ') || '无'});
}

addFooter(doc, totalPages) {
const pages = doc.bufferedPageRange();
for (let i = 0; i < pages.count; i++) {
doc.switchToPage(i);
doc.fontSize(10).font(this.fontPath);
doc.text(第 ${i + 1} / ${pages.count} 页, 50, doc.page.height - 50, { align: 'center' });
}
}

getRiskLevelText(level) {
const map = { low: '低风险', medium: '中风险', high: '高风险' };
return map[level] || level;
}
}

module.exports = new PDFService();
4. 前端历史记录页面 (frontend/src/pages/AIRecognitionHistory.js 核心部分)
jsx
import React, { useState, useEffect } from 'react';
import { Table, Tag, DatePicker, Select, Button, Space, Statistic, Row, Col } from 'antd';
import { DownloadOutlined, EyeOutlined } from '@ant-design/icons';
import EChartsReact from 'echarts-for-react';
import dayjs from 'dayjs';

const AIRecognitionHistory = () => {
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(false);
const [filters, setFilters] = useState({
startDate: dayjs().subtract(7, 'day'),
endDate: dayjs(),
riskLevel: undefined,
success: undefined
});

// 获取历史记录
const fetchLogs = async () => {
setLoading(true);
try {
const params = {
startDate: filters.startDate.format('YYYY-MM-DD'),
endDate: filters.endDate.format('YYYY-MM-DD'),
riskLevel: filters.riskLevel,
success: filters.success
};

const response = await fetch('/api/ai/logs', {
method: 'GET',
params
});
const data = await response.json();
setLogs(data.data);
} catch (error) {
message.error('获取历史记录失败');
} finally {
setLoading(false);
}
};

useEffect(() => {
fetchLogs();
}, [filters]);

// 表格列定义
const columns = [
{ title: 'ID', dataIndex: 'id', width: 80 },
{ title: '施工阶段', dataIndex: 'stage', width: 120 },
{
title: '风险等级',
dataIndex: 'riskLevel',
render: (level) => {
const colorMap = { low: 'green', medium: 'orange', high: 'red' };
return <Tag color={colorMap[level] || 'default'}>{level};
}
},
{ title: '安全问题数', dataIndex: 'safetyRisksCount', width: 100 },
{ title: '识别耗时(ms)', dataIndex: 'recognitionTime', width: 120 },
{
title: '状态',
dataIndex: 'success',
render: (success) => success ? 成功 : 失败
},
{
title: '操作',
render: (_, record) => (

<Button icon={} size="small" onClick={() => viewDetail(record)}>详情
<Button icon={} size="small" onClick={() => exportSingle(record)}>导出

)
}
];

// ECharts 统计图表配置
const chartOption = {
tooltip: { trigger: 'axis' },
legend: { data: ['成功率', '平均耗时'] },
xAxis: { type: 'category', data: logs.map(l => l.createdAt) },
yAxis: [
{ type: 'value', name: '成功率(%)' },
{ type: 'value', name: '耗时(ms)' }
],
series: [
{
name: '成功率',
type: 'line',
data: logs.map(l => l.success ? 100 : 0),
yAxisIndex: 0
},
{
name: '平均耗时',
type: 'bar',
data: logs.map(l => l.recognitionTime),
yAxisIndex: 1
}
]
};

return (
<div style={{ padding: '24px' }}>

📊 AI识别历史记录

{/* 筛选栏 */}
<Space style={{ marginBottom: 16 }}>
<DatePicker.RangePicker
value={[filters.startDate, filters.endDate]}
onChange={(dates) => setFilters({...filters, startDate: dates[0], endDate: dates[1]})}
/>
<Select placeholder="风险等级" allowClear onChange={(val) => setFilters({...filters, riskLevel: val})}>
<Select.Option value="low">低风险</Select.Option>
<Select.Option value="medium">中风险</Select.Option>
<Select.Option value="high">高风险</Select.Option>


<Button icon={} onClick={exportAll}>批量导出

{/* 统计图表 /}
<Row gutter={16} style={{ marginBottom: 24 }}>

<EChartsReact option={chartOption} style={{ height: 300 }} />



<Statistic title="平均成功率" value={(logs.filter(l=>l.success).length/logs.length
100).toFixed(1)} suffix="%" />
<Statistic title="平均耗时" value={(logs.reduce((a,b)=>a+b.recognitionTime,0)/logs.length).toFixed(0)} suffix="ms" />

{/* 数据表格 */}
<Table
columns={columns}
dataSource={logs}
loading={loading}
rowKey="id"
pagination={{ pageSize: 10 }}
/>

);
};

export default AIRecognitionHistory;

posted @ 2026-06-18 21:32  sas1996  阅读(10)  评论(0)    收藏  举报